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);