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

PHPLIB-295: arrayFilters for update and findAndModify

parent 0496705c
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
type: boolean
description: |
......
......@@ -6,6 +6,10 @@ source:
file: apiargs-MongoDBCollection-method-find-option.yaml
ref: sort
---
source:
file: apiargs-MongoDBCollection-common-option.yaml
ref: arrayFilters
---
source:
file: apiargs-MongoDBCollection-common-option.yaml
ref: collation
......
......@@ -2,6 +2,10 @@ source:
file: apiargs-MongoDBCollection-common-option.yaml
ref: upsert
---
source:
file: apiargs-MongoDBCollection-common-option.yaml
ref: arrayFilters
---
source:
file: apiargs-MongoDBCollection-common-option.yaml
ref: bypassDocumentValidation
......
......@@ -19,6 +19,16 @@ namespace MongoDB\Exception;
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.
*
......
......@@ -40,6 +40,7 @@ class BulkWrite implements Executable
const UPDATE_MANY = 'updateMany';
const UPDATE_ONE = 'updateOne';
private static $wireVersionForArrayFilters = 6;
private static $wireVersionForCollation = 5;
private static $wireVersionForDocumentLevelValidation = 4;
......@@ -47,6 +48,7 @@ class BulkWrite implements Executable
private $collectionName;
private $operations;
private $options;
private $isArrayFiltersUsed = false;
private $isCollationUsed = false;
/**
......@@ -83,6 +85,14 @@ class BulkWrite implements Executable
*
* * upsert (boolean): When true, a new document is created if no document
* 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:
*
......@@ -229,6 +239,14 @@ class BulkWrite implements Executable
$args[2]['multi'] = ($type === self::UPDATE_MANY);
$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'])) {
$this->isCollationUsed = true;
......@@ -282,11 +300,15 @@ class BulkWrite implements Executable
* @see Executable::execute()
* @param Server $server
* @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)
*/
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)) {
throw UnsupportedException::collationNotSupported();
}
......
......@@ -36,6 +36,7 @@ use MongoDB\Exception\UnsupportedException;
*/
class FindAndModify implements Executable
{
private static $wireVersionForArrayFilters = 6;
private static $wireVersionForCollation = 5;
private static $wireVersionForDocumentLevelValidation = 4;
private static $wireVersionForWriteConcern = 4;
......@@ -49,6 +50,12 @@ class FindAndModify implements Executable
*
* 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.
*
* This is not supported for server versions < 3.4 and will result in an
......@@ -105,6 +112,10 @@ class FindAndModify implements Executable
'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'])) {
throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean');
}
......@@ -173,11 +184,15 @@ class FindAndModify implements Executable
* @param Server $server
* @return array|object|null
* @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)
*/
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)) {
throw UnsupportedException::collationNotSupported();
}
......@@ -238,6 +253,10 @@ class FindAndModify implements Executable
}
}
if (isset($this->options['arrayFilters'])) {
$cmd['arrayFilters'] = $this->options['arrayFilters'];
}
if (isset($this->options['maxTimeMS'])) {
$cmd['maxTimeMS'] = $this->options['maxTimeMS'];
}
......
......@@ -41,6 +41,9 @@ class FindOneAndUpdate implements Executable
*
* 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
* circumvent document level validation.
*
......
......@@ -36,6 +36,7 @@ use MongoDB\Exception\UnsupportedException;
*/
class Update implements Executable
{
private static $wireVersionForArrayFilters = 6;
private static $wireVersionForCollation = 5;
private static $wireVersionForDocumentLevelValidation = 4;
......@@ -50,6 +51,12 @@ class Update implements Executable
*
* 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
* circumvent document level validation.
*
......@@ -93,6 +100,10 @@ class Update implements Executable
'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'])) {
throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean');
}
......@@ -134,11 +145,15 @@ class Update implements Executable
* @see Executable::execute()
* @param Server $server
* @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)
*/
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)) {
throw UnsupportedException::collationNotSupported();
}
......@@ -148,6 +163,10 @@ class Update implements Executable
'upsert' => $this->options['upsert'],
];
if (isset($this->options['arrayFilters'])) {
$updateOptions['arrayFilters'] = $this->options['arrayFilters'];
}
if (isset($this->options['collation'])) {
$updateOptions['collation'] = (object) $this->options['collation'];
}
......
......@@ -39,6 +39,12 @@ class UpdateMany implements Executable
*
* 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
* circumvent document level validation.
*
......
......@@ -39,6 +39,12 @@ class UpdateOne implements Executable
*
* 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
* circumvent document level validation.
*
......
......@@ -115,6 +115,13 @@ class CrudSpecFunctionalTest extends FunctionalTestCase
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 'find':
return $this->collection->{$operation['name']}(
......@@ -224,6 +231,38 @@ class CrudSpecFunctionalTest extends FunctionalTestCase
}
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':
$this->assertSame($expectedResult, $actualResult);
break;
......@@ -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.
*
......
......@@ -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
* @expectedExceptionMessageRegExp /Expected \$operations\[0\]\["updateMany"\]\[2\]\["collation"\] to have type "array or object" but found "[\w ]+"/
......@@ -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
* @expectedExceptionMessageRegExp /Expected \$operations\[0\]\["updateOne"\]\[2\]\["collation"\] to have type "array or object" but found "[\w ]+"/
......
......@@ -19,6 +19,10 @@ class FindAndModifyTest extends TestCase
{
$options = [];
foreach ($this->getInvalidArrayValues() as $value) {
$options[][] = ['arrayFilters' => $value];
}
foreach ($this->getInvalidBooleanValues() as $value) {
$options[][] = ['bypassDocumentValidation' => $value];
}
......
......@@ -39,6 +39,10 @@ class UpdateTest extends TestCase
{
$options = [];
foreach ($this->getInvalidArrayValues() as $value) {
$options[][] = ['arrayFilters' => $value];
}
foreach ($this->getInvalidBooleanValues() as $value) {
$options[][] = ['bypassDocumentValidation' => $value];
}
......
......@@ -11,6 +11,11 @@ use stdClass;
abstract class TestCase extends BaseTestCase
{
public function provideInvalidArrayValues()
{
return $this->wrapValuesForDataProvider($this->getInvalidArrayValues());
}
public function provideInvalidDocumentValues()
{
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