diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d8b0f9..39061d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ alpha-4 - [query] Always show path next to resultset - [node|shell] Most commands which accept a node path can also accept a UUID - [node] `node:list`: Show node primary item value +- [query] Support for UPDATE queries ### Bugs Fixes diff --git a/features/phpcr_node_edit.feature b/features/phpcr_node_edit.feature new file mode 100644 index 00000000..5d1dbb83 --- /dev/null +++ b/features/phpcr_node_edit.feature @@ -0,0 +1,26 @@ +Feature: Edit a node + In order to show some useful information about the current node + As a user that is logged into the shell + I should be able to run a command which does that + + Background: + Given that I am logged in as "testuser" + And the "session_data.xml" fixtures are loaded + + Scenario: Show node information + Given the current node is "/tests_general_base" + And I execute the "node:info daniel --no-ansi" command + Then the command should not fail + And I should see the following: + """ + +-------------------+--------------------------------------+ + | Path | /tests_general_base/daniel | + | UUID | N/A | + | Index | 1 | + | Primary node type | nt:unstructured | + | Mixin node types | | + | Checked out? | N/A | + | Locked? | [ERROR] Not implemented by jackalope | + +-------------------+--------------------------------------+ + """ + diff --git a/features/phpcr_node_property_set.feature b/features/phpcr_node_property_set.feature index 58b84460..bdf93dfe 100644 --- a/features/phpcr_node_property_set.feature +++ b/features/phpcr_node_property_set.feature @@ -12,8 +12,6 @@ Feature: Set a node property Given I execute the "" command Then the command should not fail And I save the session - And the node at "/properties" should have the property "" with value "" - Examples: | command | name | type | | node:property:set uri http://foobar | uri | http://foobar | diff --git a/features/phpcr_query_update.feature b/features/phpcr_query_update.feature new file mode 100644 index 00000000..0259375b --- /dev/null +++ b/features/phpcr_query_update.feature @@ -0,0 +1,25 @@ +Feature: Execute a a raw UPDATE query in JCR_SQL2 + In order to run an UPDATE JCR_SQL2 query easily + As a user logged into the shell + I want to simply type the query like in a normal sql shell + + Background: + Given that I am logged in as "testuser" + And the "cms.xml" fixtures are loaded + + Scenario Outline: Execute query + Given I execute the "" command + Then the command should not fail + And I save the session + And the node at "" should have the property "" with value "" + And I should see the following: + """ + 1 row(s) affected + """ + Examples: + | query | path | property | expectedValue | + | UPDATE [nt:unstructured] AS a SET a.title = 'DTL' WHERE localname() = 'article1' | /cms/articles/article1 | title | DTL | + | update [nt:unstructured] as a set a.title = 'dtl' where localname() = 'article1' | /cms/articles/article1 | title | dtl | + | UPDATE nt:unstructured AS a SET a.title = 'DTL' WHERE localname() = 'article1' | /cms/articles/article1 | title | DTL | + | UPDATE nt:unstructured AS a SET title = 'DTL' WHERE localname() = 'article1' | /cms/articles/article1 | title | DTL | + | UPDATE nt:unstructured AS a SET title = 'DTL', foobar='barfoo' WHERE localname() = 'article1' | /cms/articles/article1 | foobar | barfoo | diff --git a/spec/PHPCR/Shell/Query/UpdateParserSpec.php b/spec/PHPCR/Shell/Query/UpdateParserSpec.php new file mode 100644 index 00000000..aa690b5e --- /dev/null +++ b/spec/PHPCR/Shell/Query/UpdateParserSpec.php @@ -0,0 +1,81 @@ +beConstructedWith( + $qomf + ); + } + + function it_is_initializable() + { + $this->shouldHaveType('PHPCR\Shell\Query\UpdateParser'); + } + + function it_should_provide_a_qom_object_for_selecting( + QueryObjectModelFactoryInterface $qomf, + ChildNodeJoinConditionInterface $joinCondition, + JoinInterface $join, + SourceInterface $parentSource, + SourceInterface $childSource, + PropertyValueInterface $childValue, + LiteralInterface $literalValue, + ComparisonInterface $comparison, + QueryInterface $query + ) + { + $qomf->selector('parent', 'mgnl:page')->willReturn($parentSource); + $qomf->selector('child', 'mgnl:metaData')->willReturn($childSource); + $qomf->childNodeJoinCondition('child', 'parent')->willReturn($joinCondition); + $qomf->join($parentSource, $childSource, QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_INNER, $joinCondition)->willReturn($join); + $qomf->propertyValue('child', 'mgnl:template')->willReturn($childValue); + $qomf->literal('standard-templating-kit:stkNews')->willReturn($literalValue); + $qomf->comparison($childValue, QueryObjectModelConstantsInterface::JCR_OPERATOR_EQUAL_TO, $literalValue)->willReturn($comparison); + + $qomf->createQuery($join, $comparison)->willReturn($query); + + + $sql = <<parse($sql); + + $res->offsetGet(0)->shouldHaveType('PHPCR\Query\QueryInterface'); + $res->offsetGet(1)->shouldReturn(array( + 'parent.foo' => array( + 'selector' => 'parent', + 'name' => 'foo', + 'value' => 'PHPCR\\FOO\\Bar', + ), + 'parent.bar' => array( + 'selector' => 'parent', + 'name' => 'bar', + 'value' => 'foo', + ), + )); + } +} diff --git a/src/PHPCR/Shell/Console/Application/ShellApplication.php b/src/PHPCR/Shell/Console/Application/ShellApplication.php index 978a5e22..8b59d265 100644 --- a/src/PHPCR/Shell/Console/Application/ShellApplication.php +++ b/src/PHPCR/Shell/Console/Application/ShellApplication.php @@ -163,6 +163,7 @@ private function registerCommands() $this->add(new CommandPhpcr\SessionSaveCommand()); $this->add(new CommandPhpcr\QueryCommand()); $this->add(new CommandPhpcr\QuerySelectCommand()); + $this->add(new CommandPhpcr\QueryUpdateCommand()); $this->add(new CommandPhpcr\RetentionHoldAddCommand()); $this->add(new CommandPhpcr\RetentionHoldListCommand()); $this->add(new CommandPhpcr\RetentionHoldRemoveCommand()); diff --git a/src/PHPCR/Shell/Console/Command/Phpcr/QueryUpdateCommand.php b/src/PHPCR/Shell/Console/Command/Phpcr/QueryUpdateCommand.php new file mode 100644 index 00000000..318c698f --- /dev/null +++ b/src/PHPCR/Shell/Console/Command/Phpcr/QueryUpdateCommand.php @@ -0,0 +1,61 @@ +setName('update'); + $this->setDescription('Execute an UPDATE JCR-SQL2 query'); + $this->addArgument('query'); + $this->setHelp(<<session:save to persist changes. + +Note that this command is not part of the JCR-SQL2 language but is implemented specifically +for the PHPCR-Shell. +EOT + ); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $sql = $input->getRawCommand(); + + // trim ";" for people used to MysQL + if (substr($sql, -1) == ';') { + $sql = substr($sql, 0, -1); + } + + $session = $this->getHelper('phpcr')->getSession(); + $qm = $session->getWorkspace()->getQueryManager(); + + $updateParser = new UpdateParser($qm->getQOMFactory()); + $res = $updateParser->parse($sql); + $query = $res->offsetGet(0); + $updates = $res->offsetGet(1); + + $start = microtime(true); + $result = $query->execute(); + + foreach ($result as $row) { + foreach ($updates as $field => $property) { + $node = $row->getNode($property['selector']); + $node->setProperty($property['name'], $property['value']); + } + } + + $elapsed = microtime(true) - $start; + + $output->writeln(sprintf('%s row(s) affected in %ss', count($result), number_format($elapsed, 2))); + } +} diff --git a/src/PHPCR/Shell/Console/Input/StringInput.php b/src/PHPCR/Shell/Console/Input/StringInput.php index ad86982a..ff6bdecc 100644 --- a/src/PHPCR/Shell/Console/Input/StringInput.php +++ b/src/PHPCR/Shell/Console/Input/StringInput.php @@ -14,6 +14,7 @@ class StringInput extends BaseInput { protected $rawCommand; protected $tokens; + protected $isQuery = false; /** * {@inheritDoc} @@ -24,6 +25,12 @@ public function __construct($command) if (strpos(strtolower($this->rawCommand), 'select') === 0) { $command = 'select' . substr($command, 6); + $this->isQuery = true; + } + + if (strpos(strtolower($this->rawCommand), 'update') === 0) { + $command = 'update' . substr($command, 6); + $this->isQuery = true; } parent::__construct($command); @@ -90,10 +97,6 @@ public function getTokens() */ protected function isQuery() { - if (strpos(strtolower($this->rawCommand), 'select') === 0) { - return true; - } - - return false; + return $this->isQuery; } } diff --git a/src/PHPCR/Shell/Query/UpdateParser.php b/src/PHPCR/Shell/Query/UpdateParser.php new file mode 100644 index 00000000..b2d7686e --- /dev/null +++ b/src/PHPCR/Shell/Query/UpdateParser.php @@ -0,0 +1,128 @@ + + */ +class UpdateParser extends Sql2ToQomQueryConverter +{ + /** + * Parse an "SQL2" UPDATE statement and construct a query builder + * for selecting the rows and build a field => value mapping for the + * update. + * + * @param string $sql2 + * + * @return array($query, $updates) + */ + public function parse($sql2) + { + $this->implicitSelectorName = null; + $this->sql2 = $sql2; + $this->scanner = new Sql2Scanner($sql2); + $source = null; + $constraint = null; + + while ($this->scanner->lookupNextToken() !== '') { + switch (strtoupper($this->scanner->lookupNextToken())) { + case 'UPDATE': + $this->scanner->expectToken('UPDATE'); + $source = $this->parseSource(); + break; + case 'SET': + $this->scanner->expectToken('SET'); + $updates = $this->parseUpdates(); + break; + case 'WHERE': + $this->scanner->expectToken('WHERE'); + $constraint = $this->parseConstraint(); + break; + default: + throw new InvalidQueryException('Expected end of query, got "' . $this->scanner->lookupNextToken() . '" in ' . $this->sql2); + } + } + + if (!$source instanceof SourceInterface) { + throw new InvalidQueryException('Invalid query, source could not be determined: '.$sql2); + } + + $query = $this->factory->createQuery($source, $constraint); + + $res = new \ArrayObject(array($query, $updates)); + + return $res; + } + + /** + * Parse the SET section of the query, returning + * an array containing the property names ( , + * 'name' => , + * '' => , + * ) + * + * @return array + */ + protected function parseUpdates() + { + $updates = array(); + + while (true) { + $selectorName = $this->scanner->fetchNextToken(); + $delimiter = $this->scanner->fetchNextToken(); + + if ($delimiter !== '.') { + $property = array( + 'selector' => null, + 'name' => $selectorName + ); + $equals = $delimiter; + } else { + $property = array( + 'selector' => $selectorName, + 'name' => $this->scanner->fetchNextToken() + ); + $equals = $this->scanner->fetchNextToken(); + } + + + if ($equals !== '=') { + throw new InvalidQueryException(sprintf( + 'Expected "=" after property name in UPDATE query, got "%s"', + $equals, + $this->sql2 + )); + } + + $value = $this->parseLiteralValue(); + $property['value'] = $value; + + $updates[$property['selector'] . '.' . $property['name']] = $property; + + $next = $this->scanner->lookupNextToken(); + + if ($next == ',') { + $next = $this->scanner->fetchNextToken(); + } elseif (strtolower($next) == 'where' || !$next) { + break; + } + } + + return $updates; + } +}