Commit 762b6e90 authored by Katherine Walker's avatar Katherine Walker

PHPLIB-295: arrayFilters for update and findAndModify

parent 0496705c
arg_name: option arg_name: option
name: arrayFilters
type: array
description: |
An array of filter documents that determines which array elements to modify
for an update operation on an array field.
.. versionadded:: 1.3
interface: phpmethod
operation: ~
optional: true
---
arg_name: option
name: bypassDocumentValidation name: bypassDocumentValidation
type: boolean type: boolean
description: | description: |
......
...@@ -6,6 +6,10 @@ source: ...@@ -6,6 +6,10 @@ source:
file: apiargs-MongoDBCollection-method-find-option.yaml file: apiargs-MongoDBCollection-method-find-option.yaml
ref: sort ref: sort
--- ---
source:
file: apiargs-MongoDBCollection-common-option.yaml
ref: arrayFilters
---
source: source:
file: apiargs-MongoDBCollection-common-option.yaml file: apiargs-MongoDBCollection-common-option.yaml
ref: collation ref: collation
......
...@@ -2,6 +2,10 @@ source: ...@@ -2,6 +2,10 @@ source:
file: apiargs-MongoDBCollection-common-option.yaml file: apiargs-MongoDBCollection-common-option.yaml
ref: upsert ref: upsert
--- ---
source:
file: apiargs-MongoDBCollection-common-option.yaml
ref: arrayFilters
---
source: source:
file: apiargs-MongoDBCollection-common-option.yaml file: apiargs-MongoDBCollection-common-option.yaml
ref: bypassDocumentValidation ref: bypassDocumentValidation
......
...@@ -20,6 +20,16 @@ namespace MongoDB\Exception; ...@@ -20,6 +20,16 @@ namespace MongoDB\Exception;
class UnsupportedException extends RuntimeException class UnsupportedException extends RuntimeException
{ {
/** /**
* Thrown when array filters are not supported by a server.
*
* @return self
*/
public static function arrayFiltersNotSupported()
{
return new static('Array filters are not supported by the server executing this operation');
}
/**
* Thrown when collations are not supported by a server. * Thrown when collations are not supported by a server.
* *
* @return self * @return self
......
...@@ -40,6 +40,7 @@ class BulkWrite implements Executable ...@@ -40,6 +40,7 @@ class BulkWrite implements Executable
const UPDATE_MANY = 'updateMany'; const UPDATE_MANY = 'updateMany';
const UPDATE_ONE = 'updateOne'; const UPDATE_ONE = 'updateOne';
private static $wireVersionForArrayFilters = 6;
private static $wireVersionForCollation = 5; private static $wireVersionForCollation = 5;
private static $wireVersionForDocumentLevelValidation = 4; private static $wireVersionForDocumentLevelValidation = 4;
...@@ -47,6 +48,7 @@ class BulkWrite implements Executable ...@@ -47,6 +48,7 @@ class BulkWrite implements Executable
private $collectionName; private $collectionName;
private $operations; private $operations;
private $options; private $options;
private $isArrayFiltersUsed = false;
private $isCollationUsed = false; private $isCollationUsed = false;
/** /**
...@@ -84,6 +86,14 @@ class BulkWrite implements Executable ...@@ -84,6 +86,14 @@ class BulkWrite implements Executable
* * upsert (boolean): When true, a new document is created if no document * * upsert (boolean): When true, a new document is created if no document
* matches the query. The default is false. * matches the query. The default is false.
* *
* Supported options for updateMany and updateOne operations:
*
* * arrayFilters (document array): A set of filters specifying to which
* array elements an update should apply.
*
* This is not supported for server versions < 3.6 and will result in an
* exception at execution time if used.
*
* Supported options for the bulk write operation: * Supported options for the bulk write operation:
* *
* * bypassDocumentValidation (boolean): If true, allows the write to * * bypassDocumentValidation (boolean): If true, allows the write to
...@@ -229,6 +239,14 @@ class BulkWrite implements Executable ...@@ -229,6 +239,14 @@ class BulkWrite implements Executable
$args[2]['multi'] = ($type === self::UPDATE_MANY); $args[2]['multi'] = ($type === self::UPDATE_MANY);
$args[2] += ['upsert' => false]; $args[2] += ['upsert' => false];
if (isset($args[2]['arrayFilters'])) {
$this->isArrayFiltersUsed = true;
if ( ! is_array($args[2]['arrayFilters'])) {
throw InvalidArgumentException::InvalidType(sprintf('$operations[%d]["%s"][2]["arrayFilters"]', $i, $type), $args[2]['arrayFilters'], 'array');
}
}
if (isset($args[2]['collation'])) { if (isset($args[2]['collation'])) {
$this->isCollationUsed = true; $this->isCollationUsed = true;
...@@ -282,11 +300,15 @@ class BulkWrite implements Executable ...@@ -282,11 +300,15 @@ class BulkWrite implements Executable
* @see Executable::execute() * @see Executable::execute()
* @param Server $server * @param Server $server
* @return BulkWriteResult * @return BulkWriteResult
* @throws UnsupportedException if collation is used and unsupported * @throws UnsupportedException if array filters or collation is used and unsupported
* @throws DriverRuntimeException for other driver errors (e.g. connection errors) * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/ */
public function execute(Server $server) public function execute(Server $server)
{ {
if ($this->isArrayFiltersUsed && ! \MongoDB\server_supports_feature($server, self::$wireVersionForArrayFilters)) {
throw UnsupportedException::arrayFiltersNotSupported();
}
if ($this->isCollationUsed && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) { if ($this->isCollationUsed && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
throw UnsupportedException::collationNotSupported(); throw UnsupportedException::collationNotSupported();
} }
......
...@@ -36,6 +36,7 @@ use MongoDB\Exception\UnsupportedException; ...@@ -36,6 +36,7 @@ use MongoDB\Exception\UnsupportedException;
*/ */
class FindAndModify implements Executable class FindAndModify implements Executable
{ {
private static $wireVersionForArrayFilters = 6;
private static $wireVersionForCollation = 5; private static $wireVersionForCollation = 5;
private static $wireVersionForDocumentLevelValidation = 4; private static $wireVersionForDocumentLevelValidation = 4;
private static $wireVersionForWriteConcern = 4; private static $wireVersionForWriteConcern = 4;
...@@ -49,6 +50,12 @@ class FindAndModify implements Executable ...@@ -49,6 +50,12 @@ class FindAndModify implements Executable
* *
* Supported options: * Supported options:
* *
* * arrayFilters (document array): A set of filters specifying to which
* array elements an update should apply.
*
* This is not supported for server versions < 3.6 and will result in an
* exception at execution time if used.
*
* * collation (document): Collation specification. * * collation (document): Collation specification.
* *
* This is not supported for server versions < 3.4 and will result in an * This is not supported for server versions < 3.4 and will result in an
...@@ -105,6 +112,10 @@ class FindAndModify implements Executable ...@@ -105,6 +112,10 @@ class FindAndModify implements Executable
'upsert' => false, 'upsert' => false,
]; ];
if (isset($options['arrayFilters']) && ! is_array($options['arrayFilters'])) {
throw InvalidArgumentException::invalidType('"arrayFilters" option', $options['arrayFilters'], 'array');
}
if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) { if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) {
throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean'); throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean');
} }
...@@ -173,11 +184,15 @@ class FindAndModify implements Executable ...@@ -173,11 +184,15 @@ class FindAndModify implements Executable
* @param Server $server * @param Server $server
* @return array|object|null * @return array|object|null
* @throws UnexpectedValueException if the command response was malformed * @throws UnexpectedValueException if the command response was malformed
* @throws UnsupportedException if collation or write concern is used and unsupported * @throws UnsupportedException if array filters, collation, or write concern is used and unsupported
* @throws DriverRuntimeException for other driver errors (e.g. connection errors) * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/ */
public function execute(Server $server) public function execute(Server $server)
{ {
if (isset($this->options['arrayFilters']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForArrayFilters)) {
throw UnsupportedException::arrayFiltersNotSupported();
}
if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) { if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
throw UnsupportedException::collationNotSupported(); throw UnsupportedException::collationNotSupported();
} }
...@@ -238,6 +253,10 @@ class FindAndModify implements Executable ...@@ -238,6 +253,10 @@ class FindAndModify implements Executable
} }
} }
if (isset($this->options['arrayFilters'])) {
$cmd['arrayFilters'] = $this->options['arrayFilters'];
}
if (isset($this->options['maxTimeMS'])) { if (isset($this->options['maxTimeMS'])) {
$cmd['maxTimeMS'] = $this->options['maxTimeMS']; $cmd['maxTimeMS'] = $this->options['maxTimeMS'];
} }
......
...@@ -41,6 +41,9 @@ class FindOneAndUpdate implements Executable ...@@ -41,6 +41,9 @@ class FindOneAndUpdate implements Executable
* *
* Supported options: * Supported options:
* *
* * arrayFilters (document array): A set of filters specifying to which
* array elements an update should apply.
*
* * bypassDocumentValidation (boolean): If true, allows the write to * * bypassDocumentValidation (boolean): If true, allows the write to
* circumvent document level validation. * circumvent document level validation.
* *
......
...@@ -36,6 +36,7 @@ use MongoDB\Exception\UnsupportedException; ...@@ -36,6 +36,7 @@ use MongoDB\Exception\UnsupportedException;
*/ */
class Update implements Executable class Update implements Executable
{ {
private static $wireVersionForArrayFilters = 6;
private static $wireVersionForCollation = 5; private static $wireVersionForCollation = 5;
private static $wireVersionForDocumentLevelValidation = 4; private static $wireVersionForDocumentLevelValidation = 4;
...@@ -50,6 +51,12 @@ class Update implements Executable ...@@ -50,6 +51,12 @@ class Update implements Executable
* *
* Supported options: * Supported options:
* *
* * arrayFilters (document array): A set of filters specifying to which
* array elements an update should apply.
*
* This is not supported for server versions < 3.6 and will result in an
* exception at execution time if used.
*
* * bypassDocumentValidation (boolean): If true, allows the write to * * bypassDocumentValidation (boolean): If true, allows the write to
* circumvent document level validation. * circumvent document level validation.
* *
...@@ -93,6 +100,10 @@ class Update implements Executable ...@@ -93,6 +100,10 @@ class Update implements Executable
'upsert' => false, 'upsert' => false,
]; ];
if (isset($options['arrayFilters']) && ! is_array($options['arrayFilters'])) {
throw InvalidArgumentException::invalidType('"arrayFilters" option', $options['arrayFilters'], 'array');
}
if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) { if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) {
throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean'); throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean');
} }
...@@ -134,11 +145,15 @@ class Update implements Executable ...@@ -134,11 +145,15 @@ class Update implements Executable
* @see Executable::execute() * @see Executable::execute()
* @param Server $server * @param Server $server
* @return UpdateResult * @return UpdateResult
* @throws UnsupportedException if collation is used and unsupported * @throws UnsupportedException if array filters or collation is used and unsupported
* @throws DriverRuntimeException for other driver errors (e.g. connection errors) * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/ */
public function execute(Server $server) public function execute(Server $server)
{ {
if (isset($this->options['arrayFilters']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForArrayFilters)) {
throw UnsupportedException::arrayFiltersNotSupported();
}
if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) { if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
throw UnsupportedException::collationNotSupported(); throw UnsupportedException::collationNotSupported();
} }
...@@ -148,6 +163,10 @@ class Update implements Executable ...@@ -148,6 +163,10 @@ class Update implements Executable
'upsert' => $this->options['upsert'], 'upsert' => $this->options['upsert'],
]; ];
if (isset($this->options['arrayFilters'])) {
$updateOptions['arrayFilters'] = $this->options['arrayFilters'];
}
if (isset($this->options['collation'])) { if (isset($this->options['collation'])) {
$updateOptions['collation'] = (object) $this->options['collation']; $updateOptions['collation'] = (object) $this->options['collation'];
} }
......
...@@ -39,6 +39,12 @@ class UpdateMany implements Executable ...@@ -39,6 +39,12 @@ class UpdateMany implements Executable
* *
* Supported options: * Supported options:
* *
* * arrayFilters (document array): A set of filters specifying to which
* array elements an update should apply.
*
* This is not supported for server versions < 3.6 and will result in an$
* exception at execution time if used.
*
* * bypassDocumentValidation (boolean): If true, allows the write to * * bypassDocumentValidation (boolean): If true, allows the write to
* circumvent document level validation. * circumvent document level validation.
* *
......
...@@ -39,6 +39,12 @@ class UpdateOne implements Executable ...@@ -39,6 +39,12 @@ class UpdateOne implements Executable
* *
* Supported options: * Supported options:
* *
* * arrayFilters (document array): A set of filters specifying to which
* array elements an update should apply.
*
* This is not supported for server versions < 3.6 and will result in an$
* exception at execution time if used.
*
* * bypassDocumentValidation (boolean): If true, allows the write to * * bypassDocumentValidation (boolean): If true, allows the write to
* circumvent document level validation. * circumvent document level validation.
* *
......
...@@ -115,6 +115,13 @@ class CrudSpecFunctionalTest extends FunctionalTestCase ...@@ -115,6 +115,13 @@ class CrudSpecFunctionalTest extends FunctionalTestCase
array_diff_key($operation['arguments'], ['pipeline' => 1]) array_diff_key($operation['arguments'], ['pipeline' => 1])
); );
case 'bulkWrite':
$results = $this->prepareBulkWriteArguments($operation['arguments']);
return $this->collection->bulkWrite(
array_diff_key($results, ['options' => 1]),
$results['options']
);
case 'count': case 'count':
case 'find': case 'find':
return $this->collection->{$operation['name']}( return $this->collection->{$operation['name']}(
...@@ -224,6 +231,38 @@ class CrudSpecFunctionalTest extends FunctionalTestCase ...@@ -224,6 +231,38 @@ class CrudSpecFunctionalTest extends FunctionalTestCase
} }
break; break;
case 'bulkWrite':
if (isset($expectedResult['deletedCount'])) {
$this->assertSame($expectedResult['deletedCount'], $actualResult->getDeletedCount());
}
if (isset($expectedResult['insertedIds'])) {
$this->assertSameDocument(
['insertedIds' => $expectedResult['insertedIds']],
['insertedIds' => $actualResult->getInsertedIds()]
);
}
if (isset($expectedResult['matchedCount'])) {
$this->assertSame($expectedResult['matchedCount'], $actualResult->getMatchedCount());
}
if (isset($expectedResult['modifiedCount'])) {
$this->assertSame($expectedResult['modifiedCount'], $actualResult->getModifiedCount());
}
if (isset($expectedResult['upsertedCount'])) {
$this->assertSame($expectedResult['upsertedCount'], $actualResult->getUpsertedCount());
}
if (array_key_exists('upsertedId', $expectedResult)) {
$this->assertSameDocument(
['upsertedId' => $expectedResult['upsertedId']],
['upsertedId' => $actualResult->getUpsertedId()]
);
}
break;
case 'count': case 'count':
$this->assertSame($expectedResult, $actualResult); $this->assertSame($expectedResult, $actualResult);
break; break;
...@@ -334,6 +373,38 @@ class CrudSpecFunctionalTest extends FunctionalTestCase ...@@ -334,6 +373,38 @@ class CrudSpecFunctionalTest extends FunctionalTestCase
} }
} }
private function prepareBulkWriteArguments($arguments)
{
$operations = [];
$operations['options'] = $arguments['options'];
foreach ($arguments['requests'] as $request) {
$innerArray = [];
switch ($request['name']) {
case 'deleteMany':
case 'deleteOne':
$options = array_diff_key($request['arguments'], ['filter' => 1]);
$innerArray = [$request['arguments']['filter'], $options];
break;
case 'insertOne':
$innerArray = [$request['arguments']['document']];
break;
case 'replaceOne':
$options = array_diff_key($request['arguments'], ['filter' => 1, 'replacement' => 1]);
$innerArray = [$request['arguments']['filter'], $request['arguments']['replacement'], $options];
break;
case 'updateMany':
case 'updateOne':
$options = array_diff_key($request['arguments'], ['filter' => 1, 'update' => 1]);
$innerArray = [$request['arguments']['filter'], $request['arguments']['update'], $options];
break;
default:
throw new LogicException('Unsupported bulk write request: ' . $request['name']);
}
$operations[] = [$request['name'] => $innerArray];
}
return $operations;
}
/** /**
* Prepares arguments for findOneAndReplace and findOneAndUpdate operations. * Prepares arguments for findOneAndReplace and findOneAndUpdate operations.
* *
......
...@@ -292,6 +292,18 @@ class BulkWriteTest extends TestCase ...@@ -292,6 +292,18 @@ class BulkWriteTest extends TestCase
]); ]);
} }
/**
* @expectedException MongoDB\Exception\InvalidArgumentException
* @expectedExceptionMessageRegExp /Expected \$operations\[0\]\["updateMany"\]\[2\]\["arrayFilters"\] to have type "array" but found "[\w ]+"/
* @dataProvider provideInvalidArrayValues
*/
public function testUpdateManyArrayFiltersOptionTypeCheck($arrayFilters)
{
new BulkWrite($this->getDatabaseName(), $this->getCollectionName(), [
[BulkWrite::UPDATE_MANY => [['x' => 1], ['$set' => ['x' => 1]], ['arrayFilters' => $arrayFilters]]],
]);
}
/** /**
* @expectedException MongoDB\Exception\InvalidArgumentException * @expectedException MongoDB\Exception\InvalidArgumentException
* @expectedExceptionMessageRegExp /Expected \$operations\[0\]\["updateMany"\]\[2\]\["collation"\] to have type "array or object" but found "[\w ]+"/ * @expectedExceptionMessageRegExp /Expected \$operations\[0\]\["updateMany"\]\[2\]\["collation"\] to have type "array or object" but found "[\w ]+"/
...@@ -373,6 +385,18 @@ class BulkWriteTest extends TestCase ...@@ -373,6 +385,18 @@ class BulkWriteTest extends TestCase
]); ]);
} }
/**
* @expectedException MongoDB\Exception\InvalidArgumentException
* @expectedExceptionMessageRegExp /Expected \$operations\[0\]\["updateOne"\]\[2\]\["arrayFilters"\] to have type "array" but found "[\w ]+"/
* @dataProvider provideInvalidArrayValues
*/
public function testUpdateOneArrayFiltersOptionTypeCheck($arrayFilters)
{
new BulkWrite($this->getDatabaseName(), $this->getCollectionName(), [
[BulkWrite::UPDATE_ONE => [['x' => 1], ['$set' => ['x' => 1]], ['arrayFilters' => $arrayFilters]]],
]);
}
/** /**
* @expectedException MongoDB\Exception\InvalidArgumentException * @expectedException MongoDB\Exception\InvalidArgumentException
* @expectedExceptionMessageRegExp /Expected \$operations\[0\]\["updateOne"\]\[2\]\["collation"\] to have type "array or object" but found "[\w ]+"/ * @expectedExceptionMessageRegExp /Expected \$operations\[0\]\["updateOne"\]\[2\]\["collation"\] to have type "array or object" but found "[\w ]+"/
......
...@@ -19,6 +19,10 @@ class FindAndModifyTest extends TestCase ...@@ -19,6 +19,10 @@ class FindAndModifyTest extends TestCase
{ {
$options = []; $options = [];
foreach ($this->getInvalidArrayValues() as $value) {
$options[][] = ['arrayFilters' => $value];
}
foreach ($this->getInvalidBooleanValues() as $value) { foreach ($this->getInvalidBooleanValues() as $value) {
$options[][] = ['bypassDocumentValidation' => $value]; $options[][] = ['bypassDocumentValidation' => $value];
} }
......
...@@ -39,6 +39,10 @@ class UpdateTest extends TestCase ...@@ -39,6 +39,10 @@ class UpdateTest extends TestCase
{ {
$options = []; $options = [];
foreach ($this->getInvalidArrayValues() as $value) {
$options[][] = ['arrayFilters' => $value];
}
foreach ($this->getInvalidBooleanValues() as $value) { foreach ($this->getInvalidBooleanValues() as $value) {
$options[][] = ['bypassDocumentValidation' => $value]; $options[][] = ['bypassDocumentValidation' => $value];
} }
......
...@@ -11,6 +11,11 @@ use stdClass; ...@@ -11,6 +11,11 @@ use stdClass;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
public function provideInvalidArrayValues()
{
return $this->wrapValuesForDataProvider($this->getInvalidArrayValues());
}
public function provideInvalidDocumentValues() public function provideInvalidDocumentValues()
{ {
return $this->wrapValuesForDataProvider($this->getInvalidDocumentValues()); return $this->wrapValuesForDataProvider($this->getInvalidDocumentValues());
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment