diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ba5d7d..611ca065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ dev-master ### Features +- [query:update] Added APPLY method to queries. +- [query:update] APPLY `mixin_add` and `mixin_remove` functions - [node:remove] Immediately fail when trying to delete a node which has a (hard) referrer - [cli] Specify workspace with first argument diff --git a/features/all/phpcr_node_edit.feature b/features/all/phpcr_node_edit.feature index aaae6311..41209f7c 100644 --- a/features/all/phpcr_node_edit.feature +++ b/features/all/phpcr_node_edit.feature @@ -106,11 +106,11 @@ Feature: Edit a node type: String value: 'FOOOOOOO' """ - And I execute the "node:edit cms/products/product2" command + And I execute the "node:edit cms/products/productx" command Then the command should not fail And I save the session Then the command should not fail - And the property "/cms/products/product2/foobar" should have type "String" and value "FOOOOOOO" + And the property "/cms/products/productx/foobar" should have type "String" and value "FOOOOOOO" Scenario: Create a new node with short syntax Given I have an editor which produces the following: @@ -120,11 +120,11 @@ Feature: Edit a node value: 'nt:unstructured' foobar: FOOOOOOO """ - And I execute the "node:edit cms/products/product2" command + And I execute the "node:edit cms/products/productx" command Then the command should not fail And I save the session Then the command should not fail - And the property "/cms/products/product2/foobar" should have type "String" and value "FOOOOOOO" + And the property "/cms/products/productx/foobar" should have type "String" and value "FOOOOOOO" Scenario: Create a new node with a specified type Given I have an editor which produces the following: @@ -136,18 +136,18 @@ Feature: Edit a node type: Binary value: foo """ - And I execute the "node:edit cms/products/product2 --type=nt:resource" command + And I execute the "node:edit cms/products/productx --type=nt:resource" command Then the command should not fail And I save the session Then the command should not fail - And there should exist a node at "/cms/products/product2" - And the primary type of "/cms/products/product2" should be "nt:resource" + And there should exist a node at "/cms/products/productx" + And the primary type of "/cms/products/productx" should be "nt:resource" Scenario: Editor returns empty string Given I have an editor which produces the following: """" """ - And I execute the "node:edit cms/products/product2 --no-interaction --type=nt:resource" command + And I execute the "node:edit cms/products/productx --no-interaction --type=nt:resource" command Then the command should fail Scenario: Edit a node by UUID diff --git a/features/all/phpcr_query_update.feature b/features/all/phpcr_query_update.feature index 16dbc606..f4e92b05 100644 --- a/features/all/phpcr_query_update.feature +++ b/features/all/phpcr_query_update.feature @@ -113,10 +113,30 @@ Feature: Execute a a raw UPDATE query in JCR_SQL2 [PHPCR\PathNotFoundException] Property 10 """ - Scenario: Replace a multivalue property by invalid index with array (invalid) - Given I execute the "UPDATE [nt:unstructured] AS a SET a.tags = array_replace_at(a.tags, 0, array('Kite')) WHERE a.tags = 'Planes'" command - Then the command should fail - And I should see the following: - """ - Cannot use an array as a value in a multivalue property - """ + Scenario: Apply mixin_remove + Given I execute the "UPDATE [nt:unstructured] AS a APPLY mixin_remove('mix:title') WHERE a.name = 'Product Two'" command + Then the command should not fail + And I save the session + Then the command should not fail + Then the node at "/cms/products/product2" should not have the mixin "mix:title" + + Scenario: Apply mixin_add + Given I execute the "UPDATE [nt:unstructured] AS a APPLY mixin_add('mix:mimeType') WHERE a.tags = 'Planes'" command + Then the command should not fail + And I save the session + And the node at "/cms/articles/article1" should have the mixin "mix:mimeType" + + Scenario: Apply mixin_add existing + Given I execute the "UPDATE [nt:unstructured] AS a APPLY mixin_add('mix:title') WHERE a.name = 'Product Two'" command + Then the command should not fail + And I save the session + Then the command should not fail + Then the node at "/cms/products/product2" should have the mixin "mix:title" + + Scenario: Apply multiple functions + Given I execute the "UPDATE [nt:unstructured] AS a APPLY mixin_add('mix:mimeType'), mixin_add('mix:lockable') WHERE a.tags = 'Planes'" command + Then the command should not fail + And I save the session + And the node at "/cms/articles/article1" should have the mixin "mix:mimeType" + Then the node at "/cms/articles/article1" should have the mixin "mix:lockable" + diff --git a/features/fixtures/cms.xml b/features/fixtures/cms.xml index 51de2d96..a5e4bb8d 100644 --- a/features/fixtures/cms.xml +++ b/features/fixtures/cms.xml @@ -58,6 +58,17 @@ 99999999-1abf-4708-bfcc-e49511754b40 + + + nt:unstructured + + + mix:title + + + Product Two + + diff --git a/spec/PHPCR/Shell/Query/UpdateParserSpec.php b/spec/PHPCR/Shell/Query/UpdateParserSpec.php index 7c122bf4..d10fb4a8 100644 --- a/spec/PHPCR/Shell/Query/UpdateParserSpec.php +++ b/spec/PHPCR/Shell/Query/UpdateParserSpec.php @@ -94,4 +94,22 @@ public function it_should_parse_functions ( $res->offsetGet(0)->shouldHaveType('PHPCR\Query\QueryInterface'); } + + public function it_should_parse_apply ( + QueryObjectModelFactoryInterface $qomf, + SourceInterface $source, + QueryInterface $query + ) + { + $qomf->selector('a', 'dtl:article')->willReturn($source); + $qomf->createQuery($source, null)->willReturn($query); + + + $sql = <<parse($sql); + + $res->offsetGet(0)->shouldHaveType('PHPCR\Query\QueryInterface'); + } } diff --git a/src/PHPCR/Shell/Console/Command/Phpcr/QueryUpdateCommand.php b/src/PHPCR/Shell/Console/Command/Phpcr/QueryUpdateCommand.php index ca6e85c6..7256cb13 100644 --- a/src/PHPCR/Shell/Console/Command/Phpcr/QueryUpdateCommand.php +++ b/src/PHPCR/Shell/Console/Command/Phpcr/QueryUpdateCommand.php @@ -74,6 +74,7 @@ public function execute(InputInterface $input, OutputInterface $output) $res = $updateParser->parse($sql); $query = $res->offsetGet(0); $updates = $res->offsetGet(1); + $applies = $res->offsetGet(3); $start = microtime(true); $result = $query->execute(); @@ -84,7 +85,11 @@ public function execute(InputInterface $input, OutputInterface $output) foreach ($result as $row) { $rows++; foreach ($updates as $property) { - $updateProcessor->updateNode($row, $property); + $updateProcessor->updateNodeSet($row, $property); + } + + foreach ($applies as $apply) { + $updateProcessor->updateNodeApply($row, $apply); } } diff --git a/src/PHPCR/Shell/Query/FunctionOperand.php b/src/PHPCR/Shell/Query/FunctionOperand.php index 8be1f28e..08fb05a0 100644 --- a/src/PHPCR/Shell/Query/FunctionOperand.php +++ b/src/PHPCR/Shell/Query/FunctionOperand.php @@ -60,6 +60,7 @@ public function execute($functionMap, $row) $callable = $functionMap[$functionName]; $args = $this->getArguments(); + array_unshift($args, $row); array_unshift($args, $this); $value = call_user_func_array($callable, $args); diff --git a/src/PHPCR/Shell/Query/UpdateParser.php b/src/PHPCR/Shell/Query/UpdateParser.php index 6a73982b..0622626f 100644 --- a/src/PHPCR/Shell/Query/UpdateParser.php +++ b/src/PHPCR/Shell/Query/UpdateParser.php @@ -40,6 +40,8 @@ private function doParse($sql2) $this->sql2 = $sql2; $source = null; $constraint = null; + $updates = array(); + $applies = array(); while ($this->scanner->lookupNextToken() !== '') { switch (strtoupper($this->scanner->lookupNextToken())) { @@ -51,6 +53,10 @@ private function doParse($sql2) $this->scanner->expectToken('SET'); $updates = $this->parseUpdates(); break; + case 'APPLY': + $this->scanner->expectToken('APPLY'); + $applies = $this->parseApply(); + break; case 'WHERE': $this->scanner->expectToken('WHERE'); $constraint = $this->parseConstraint(); @@ -66,7 +72,7 @@ private function doParse($sql2) $query = $this->factory->createQuery($source, $constraint); - $res = new \ArrayObject(array($query, $updates, $constraint)); + $res = new \ArrayObject(array($query, $updates, $constraint, $applies)); return $res; } @@ -164,6 +170,31 @@ private function parseOperand() return new ColumnOperand($columnData[0], $columnData[1]); } + private function parseApply() + { + $functions = array(); + + while (true) { + $token = strtoupper($this->scanner->lookupNextToken()); + + if ($this->scanner->lookupNextToken(1) == '(') { + $functionData = $this->parseFunction(); + + $functions[] = new FunctionOperand($functionData[0], $functionData[1]); + } + + $next = $this->scanner->lookupNextToken(); + + if ($next == ',') { + $next = $this->scanner->fetchNextToken(); + } elseif (strtolower($next) == 'where' || !$next) { + break; + } + } + + return $functions; + } + private function parseFunction() { $functionName = $this->scanner->fetchNextToken(); diff --git a/src/PHPCR/Shell/Query/UpdateProcessor.php b/src/PHPCR/Shell/Query/UpdateProcessor.php index 103f5421..742b34a0 100644 --- a/src/PHPCR/Shell/Query/UpdateProcessor.php +++ b/src/PHPCR/Shell/Query/UpdateProcessor.php @@ -3,6 +3,7 @@ namespace PHPCR\Shell\Query; use PHPCR\Query\RowInterface; +use PHPCR\Shell\Query\FunctionOperand; /** * Processor for node updates @@ -18,8 +19,22 @@ class UpdateProcessor public function __construct() { - $this->functionMap = array( - 'array_replace' => function ($operand, $v, $x, $y) { + $this->functionMapApply = array( + 'mixin_add' => function ($operand, $row, $mixinName) { + $node = $row->getNode(); + $node->addMixin($mixinName); + }, + 'mixin_remove' => function ($operand, $row, $mixinName) { + $node = $row->getNode(); + + if ($node->isNodeType($mixinName)) { + $node->removeMixin($mixinName); + } + } + ); + + $this->functionMapSet = array( + 'array_replace' => function ($operand, $row, $v, $x, $y) { $operand->validateScalarArray($v); foreach ($v as $key => $value) { if ($value === $x) { @@ -29,7 +44,7 @@ public function __construct() return $v; }, - 'array_remove' => function ($operand, $v, $x) { + 'array_remove' => function ($operand, $row, $v, $x) { foreach ($v as $key => $value) { if ($value === $x) { unset($v[$key]); @@ -38,7 +53,7 @@ public function __construct() return array_values($v); }, - 'array_append' => function ($operand, $v, $x) { + 'array_append' => function ($operand, $row, $v, $x) { $operand->validateScalarArray($v); $v[] = $x; @@ -49,10 +64,12 @@ public function __construct() // first argument is the operand array_shift($values); + // second is the row + array_shift($values); return $values; }, - 'array_replace_at' => function ($operand, $current, $index, $value) { + 'array_replace_at' => function ($operand, $row, $current, $index, $value) { if (!isset($current[$index])) { throw new \InvalidArgumentException(sprintf( 'Multivalue index "%s" does not exist', @@ -81,22 +98,32 @@ public function __construct() * @param PHPCR\Query\RowInterface * @param array */ - public function updateNode(RowInterface $row, $propertyData) + public function updateNodeSet(RowInterface $row, $propertyData) { $node = $row->getNode($propertyData['selector']); $value = $propertyData['value']; if ($value instanceof FunctionOperand) { - $value = $this->handleFunction($row, $propertyData); + $value = $propertyData['value']; + $value = $value->execute($this->functionMapSet, $row); } $node->setProperty($propertyData['name'], $value); } + public function updateNodeApply(RowInterface $row, FunctionOperand $apply) + { + if (!$apply instanceof FunctionOperand) { + throw new \InvalidArgumentException( + 'Was expecting a function operand but got something else' + ); + } + + $apply->execute($this->functionMapApply, $row); + } + private function handleFunction($row, $propertyData) { - $value = $propertyData['value']; - $value = $value->execute($this->functionMap, $row); return $value; } diff --git a/src/PHPCR/Shell/Test/ContextBase.php b/src/PHPCR/Shell/Test/ContextBase.php index f69cc6d6..cf0fe7af 100644 --- a/src/PHPCR/Shell/Test/ContextBase.php +++ b/src/PHPCR/Shell/Test/ContextBase.php @@ -218,6 +218,7 @@ public function theFixturesAreLoaded($arg1) NodeHelper::purgeWorkspace($session); $session->save(); + // shouldn't have to do this, but this seems to be a bug in jackalope $session->refresh(false);