Skip to content

Commit a0808ce

Browse files
committed
Merge pull request #68 from phpcr/update_parser
Added class which parses UPDATE queries
2 parents 6734e5e + 5294445 commit a0808ce

File tree

9 files changed

+331
-7
lines changed

9 files changed

+331
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ alpha-4
1010
- [query] Always show path next to resultset
1111
- [node|shell] Most commands which accept a node path can also accept a UUID
1212
- [node] `node:list`: Show node primary item value
13+
- [query] Support for UPDATE queries
1314

1415
### Bugs Fixes
1516

features/phpcr_node_edit.feature

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Feature: Edit a node
2+
In order to show some useful information about the current node
3+
As a user that is logged into the shell
4+
I should be able to run a command which does that
5+
6+
Background:
7+
Given that I am logged in as "testuser"
8+
And the "session_data.xml" fixtures are loaded
9+
10+
Scenario: Show node information
11+
Given the current node is "/tests_general_base"
12+
And I execute the "node:info daniel --no-ansi" command
13+
Then the command should not fail
14+
And I should see the following:
15+
"""
16+
+-------------------+--------------------------------------+
17+
| Path | /tests_general_base/daniel |
18+
| UUID | N/A |
19+
| Index | 1 |
20+
| Primary node type | nt:unstructured |
21+
| Mixin node types | |
22+
| Checked out? | N/A |
23+
| Locked? | [ERROR] Not implemented by jackalope |
24+
+-------------------+--------------------------------------+
25+
"""
26+

features/phpcr_node_property_set.feature

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ Feature: Set a node property
1212
Given I execute the "<command>" command
1313
Then the command should not fail
1414
And I save the session
15-
And the node at "/properties" should have the property "<name>" with value "<type>"
16-
1715
Examples:
1816
| command | name | type |
1917
| node:property:set uri http://foobar | uri | http://foobar |

features/phpcr_query_update.feature

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Feature: Execute a a raw UPDATE query in JCR_SQL2
2+
In order to run an UPDATE JCR_SQL2 query easily
3+
As a user logged into the shell
4+
I want to simply type the query like in a normal sql shell
5+
6+
Background:
7+
Given that I am logged in as "testuser"
8+
And the "cms.xml" fixtures are loaded
9+
10+
Scenario Outline: Execute query
11+
Given I execute the "<query>" command
12+
Then the command should not fail
13+
And I save the session
14+
And the node at "<path>" should have the property "<property>" with value "<expectedValue>"
15+
And I should see the following:
16+
"""
17+
1 row(s) affected
18+
"""
19+
Examples:
20+
| query | path | property | expectedValue |
21+
| UPDATE [nt:unstructured] AS a SET a.title = 'DTL' WHERE localname() = 'article1' | /cms/articles/article1 | title | DTL |
22+
| update [nt:unstructured] as a set a.title = 'dtl' where localname() = 'article1' | /cms/articles/article1 | title | dtl |
23+
| UPDATE nt:unstructured AS a SET a.title = 'DTL' WHERE localname() = 'article1' | /cms/articles/article1 | title | DTL |
24+
| UPDATE nt:unstructured AS a SET title = 'DTL' WHERE localname() = 'article1' | /cms/articles/article1 | title | DTL |
25+
| UPDATE nt:unstructured AS a SET title = 'DTL', foobar='barfoo' WHERE localname() = 'article1' | /cms/articles/article1 | foobar | barfoo |
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
namespace spec\PHPCR\Shell\Query;
4+
5+
use PhpSpec\ObjectBehavior;
6+
use Prophecy\Argument;
7+
use PHPCR\Query\QOM\QueryObjectModelFactoryInterface;
8+
use PHPCR\Query\QOM\JoinInterface;
9+
use PHPCR\Query\QOM\SourceInterface;
10+
use PHPCR\Query\QOM\ChildNodeJoinConditionInterface;
11+
use PHPCR\Query\QOM\QueryObjectModelConstantsInterface;
12+
use PHPCR\Query\QOM\PropertyValueInterface;
13+
use PHPCR\Query\QOM\LiteralInterface;
14+
use PHPCR\Query\QOM\ComparisonInterface;
15+
use PHPCR\Query\QueryInterface;
16+
17+
class UpdateParserSpec extends ObjectBehavior
18+
{
19+
function let(
20+
QueryObjectModelFactoryInterface $qomf
21+
)
22+
{
23+
$this->beConstructedWith(
24+
$qomf
25+
);
26+
}
27+
28+
function it_is_initializable()
29+
{
30+
$this->shouldHaveType('PHPCR\Shell\Query\UpdateParser');
31+
}
32+
33+
function it_should_provide_a_qom_object_for_selecting(
34+
QueryObjectModelFactoryInterface $qomf,
35+
ChildNodeJoinConditionInterface $joinCondition,
36+
JoinInterface $join,
37+
SourceInterface $parentSource,
38+
SourceInterface $childSource,
39+
PropertyValueInterface $childValue,
40+
LiteralInterface $literalValue,
41+
ComparisonInterface $comparison,
42+
QueryInterface $query
43+
)
44+
{
45+
$qomf->selector('parent', 'mgnl:page')->willReturn($parentSource);
46+
$qomf->selector('child', 'mgnl:metaData')->willReturn($childSource);
47+
$qomf->childNodeJoinCondition('child', 'parent')->willReturn($joinCondition);
48+
$qomf->join($parentSource, $childSource, QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_INNER, $joinCondition)->willReturn($join);
49+
$qomf->propertyValue('child', 'mgnl:template')->willReturn($childValue);
50+
$qomf->literal('standard-templating-kit:stkNews')->willReturn($literalValue);
51+
$qomf->comparison($childValue, QueryObjectModelConstantsInterface::JCR_OPERATOR_EQUAL_TO, $literalValue)->willReturn($comparison);
52+
53+
$qomf->createQuery($join, $comparison)->willReturn($query);
54+
55+
56+
$sql = <<<EOT
57+
UPDATE [mgnl:page] AS parent
58+
INNER JOIN [mgnl:metaData] AS child ON ISCHILDNODE(child,parent)
59+
SET
60+
parent.foo = 'PHPCR\\FOO\\Bar',
61+
parent.bar = 'foo'
62+
WHERE
63+
child.[mgnl:template] = 'standard-templating-kit:stkNews'
64+
EOT;
65+
$res = $this->parse($sql);
66+
67+
$res->offsetGet(0)->shouldHaveType('PHPCR\Query\QueryInterface');
68+
$res->offsetGet(1)->shouldReturn(array(
69+
'parent.foo' => array(
70+
'selector' => 'parent',
71+
'name' => 'foo',
72+
'value' => 'PHPCR\\FOO\\Bar',
73+
),
74+
'parent.bar' => array(
75+
'selector' => 'parent',
76+
'name' => 'bar',
77+
'value' => 'foo',
78+
),
79+
));
80+
}
81+
}

src/PHPCR/Shell/Console/Application/ShellApplication.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ private function registerCommands()
163163
$this->add(new CommandPhpcr\SessionSaveCommand());
164164
$this->add(new CommandPhpcr\QueryCommand());
165165
$this->add(new CommandPhpcr\QuerySelectCommand());
166+
$this->add(new CommandPhpcr\QueryUpdateCommand());
166167
$this->add(new CommandPhpcr\RetentionHoldAddCommand());
167168
$this->add(new CommandPhpcr\RetentionHoldListCommand());
168169
$this->add(new CommandPhpcr\RetentionHoldRemoveCommand());
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace PHPCR\Shell\Console\Command\Phpcr;
4+
5+
use Symfony\Component\Console\Command\Command;
6+
use Symfony\Component\Console\Input\InputInterface;
7+
use Symfony\Component\Console\Output\OutputInterface;
8+
use PHPCR\Shell\Query\UpdateParser;
9+
10+
class QueryUpdateCommand extends Command
11+
{
12+
protected function configure()
13+
{
14+
$this->setName('update');
15+
$this->setDescription('Execute an UPDATE JCR-SQL2 query');
16+
$this->addArgument('query');
17+
$this->setHelp(<<<EOT
18+
Execute a JCR-SQL2 update query. Unlike other commands you can enter a query literally:
19+
20+
UPDATE [nt:unstructured] AS a SET title = 'foobar' WHERE a.title = 'barfoo';
21+
22+
You must call <info>session:save</info> to persist changes.
23+
24+
Note that this command is not part of the JCR-SQL2 language but is implemented specifically
25+
for the PHPCR-Shell.
26+
EOT
27+
);
28+
}
29+
30+
public function execute(InputInterface $input, OutputInterface $output)
31+
{
32+
$sql = $input->getRawCommand();
33+
34+
// trim ";" for people used to MysQL
35+
if (substr($sql, -1) == ';') {
36+
$sql = substr($sql, 0, -1);
37+
}
38+
39+
$session = $this->getHelper('phpcr')->getSession();
40+
$qm = $session->getWorkspace()->getQueryManager();
41+
42+
$updateParser = new UpdateParser($qm->getQOMFactory());
43+
$res = $updateParser->parse($sql);
44+
$query = $res->offsetGet(0);
45+
$updates = $res->offsetGet(1);
46+
47+
$start = microtime(true);
48+
$result = $query->execute();
49+
50+
foreach ($result as $row) {
51+
foreach ($updates as $field => $property) {
52+
$node = $row->getNode($property['selector']);
53+
$node->setProperty($property['name'], $property['value']);
54+
}
55+
}
56+
57+
$elapsed = microtime(true) - $start;
58+
59+
$output->writeln(sprintf('%s row(s) affected in %ss', count($result), number_format($elapsed, 2)));
60+
}
61+
}

src/PHPCR/Shell/Console/Input/StringInput.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class StringInput extends BaseInput
1414
{
1515
protected $rawCommand;
1616
protected $tokens;
17+
protected $isQuery = false;
1718

1819
/**
1920
* {@inheritDoc}
@@ -24,6 +25,12 @@ public function __construct($command)
2425

2526
if (strpos(strtolower($this->rawCommand), 'select') === 0) {
2627
$command = 'select' . substr($command, 6);
28+
$this->isQuery = true;
29+
}
30+
31+
if (strpos(strtolower($this->rawCommand), 'update') === 0) {
32+
$command = 'update' . substr($command, 6);
33+
$this->isQuery = true;
2734
}
2835

2936
parent::__construct($command);
@@ -90,10 +97,6 @@ public function getTokens()
9097
*/
9198
protected function isQuery()
9299
{
93-
if (strpos(strtolower($this->rawCommand), 'select') === 0) {
94-
return true;
95-
}
96-
97-
return false;
100+
return $this->isQuery;
98101
}
99102
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
namespace PHPCR\Shell\Query;
4+
5+
use PHPCR\Util\ValueConverter;
6+
use PHPCR\Util\QOM\Sql2Scanner;
7+
use PHPCR\Util\QOM\Sql2ToQomQueryConverter;
8+
use PHPCR\Query\InvalidQueryException;
9+
use PHPCR\Query\QOM\SourceInterface;
10+
11+
/**
12+
* Parse "UPDATE" queries.
13+
*
14+
* This class extends the Sql2ToQomQueryConverter class and adapts it
15+
* to parse UPDATE queries.
16+
*
17+
* @author Daniel Leech <daniel@dantleech.com>
18+
*/
19+
class UpdateParser extends Sql2ToQomQueryConverter
20+
{
21+
/**
22+
* Parse an "SQL2" UPDATE statement and construct a query builder
23+
* for selecting the rows and build a field => value mapping for the
24+
* update.
25+
*
26+
* @param string $sql2
27+
*
28+
* @return array($query, $updates)
29+
*/
30+
public function parse($sql2)
31+
{
32+
$this->implicitSelectorName = null;
33+
$this->sql2 = $sql2;
34+
$this->scanner = new Sql2Scanner($sql2);
35+
$source = null;
36+
$constraint = null;
37+
38+
while ($this->scanner->lookupNextToken() !== '') {
39+
switch (strtoupper($this->scanner->lookupNextToken())) {
40+
case 'UPDATE':
41+
$this->scanner->expectToken('UPDATE');
42+
$source = $this->parseSource();
43+
break;
44+
case 'SET':
45+
$this->scanner->expectToken('SET');
46+
$updates = $this->parseUpdates();
47+
break;
48+
case 'WHERE':
49+
$this->scanner->expectToken('WHERE');
50+
$constraint = $this->parseConstraint();
51+
break;
52+
default:
53+
throw new InvalidQueryException('Expected end of query, got "' . $this->scanner->lookupNextToken() . '" in ' . $this->sql2);
54+
}
55+
}
56+
57+
if (!$source instanceof SourceInterface) {
58+
throw new InvalidQueryException('Invalid query, source could not be determined: '.$sql2);
59+
}
60+
61+
$query = $this->factory->createQuery($source, $constraint);
62+
63+
$res = new \ArrayObject(array($query, $updates));
64+
65+
return $res;
66+
}
67+
68+
/**
69+
* Parse the SET section of the query, returning
70+
* an array containing the property names (<selectorName.propertyName)
71+
* as keys and an array
72+
*
73+
* array(
74+
* 'selector' => <selector>,
75+
* 'name' => <name>,
76+
* '<value>' => <property value>,
77+
* )
78+
*
79+
* @return array
80+
*/
81+
protected function parseUpdates()
82+
{
83+
$updates = array();
84+
85+
while (true) {
86+
$selectorName = $this->scanner->fetchNextToken();
87+
$delimiter = $this->scanner->fetchNextToken();
88+
89+
if ($delimiter !== '.') {
90+
$property = array(
91+
'selector' => null,
92+
'name' => $selectorName
93+
);
94+
$equals = $delimiter;
95+
} else {
96+
$property = array(
97+
'selector' => $selectorName,
98+
'name' => $this->scanner->fetchNextToken()
99+
);
100+
$equals = $this->scanner->fetchNextToken();
101+
}
102+
103+
104+
if ($equals !== '=') {
105+
throw new InvalidQueryException(sprintf(
106+
'Expected "=" after property name in UPDATE query, got "%s"',
107+
$equals,
108+
$this->sql2
109+
));
110+
}
111+
112+
$value = $this->parseLiteralValue();
113+
$property['value'] = $value;
114+
115+
$updates[$property['selector'] . '.' . $property['name']] = $property;
116+
117+
$next = $this->scanner->lookupNextToken();
118+
119+
if ($next == ',') {
120+
$next = $this->scanner->fetchNextToken();
121+
} elseif (strtolower($next) == 'where' || !$next) {
122+
break;
123+
}
124+
}
125+
126+
return $updates;
127+
}
128+
}

0 commit comments

Comments
 (0)