Skip to content

Commit 694a2c2

Browse files
committed
PHPLIB-1492: Support sort option for updateOne and replaceOne
Addresses both single-statement updates and bulk writes.
1 parent d1aea99 commit 694a2c2

File tree

8 files changed

+83
-9
lines changed

8 files changed

+83
-9
lines changed

src/Operation/BulkWrite.php

+20
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ class BulkWrite implements Executable
9191
* * upsert (boolean): When true, a new document is created if no document
9292
* matches the query. The default is false.
9393
*
94+
* Supported options for replaceOne and updateOne operations:
95+
*
96+
* * sort (document): Determines which document the operation modifies if
97+
* the query selects multiple documents.
98+
*
99+
* This is not supported for server versions < 8.0 and will result in an
100+
* exception at execution time if used.
101+
*
94102
* Supported options for updateMany and updateOne operations:
95103
*
96104
* * arrayFilters (document array): A set of filters specifying to which
@@ -372,6 +380,10 @@ private function validateOperations(array $operations, ?DocumentCodec $codec, En
372380
throw InvalidArgumentException::expectedDocumentType(sprintf('$operations[%d]["%s"][2]["collation"]', $i, $type), $args[2]['collation']);
373381
}
374382

383+
if (isset($args[2]['sort']) && ! is_document($args[2]['sort'])) {
384+
throw InvalidArgumentException::expectedDocumentType(sprintf('$operations[%d]["%s"][2]["sort"]', $i, $type), $args[2]['sort']);
385+
}
386+
375387
if (! is_bool($args[2]['upsert'])) {
376388
throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]["upsert"]', $i, $type), $args[2]['upsert'], 'boolean');
377389
}
@@ -413,6 +425,14 @@ private function validateOperations(array $operations, ?DocumentCodec $codec, En
413425
throw InvalidArgumentException::expectedDocumentType(sprintf('$operations[%d]["%s"][2]["collation"]', $i, $type), $args[2]['collation']);
414426
}
415427

428+
if (isset($args[2]['sort']) && ! is_document($args[2]['sort'])) {
429+
throw InvalidArgumentException::expectedDocumentType(sprintf('$operations[%d]["%s"][2]["sort"]', $i, $type), $args[2]['sort']);
430+
}
431+
432+
if (isset($args[2]['sort']) && $args[2]['multi']) {
433+
throw new InvalidArgumentException(sprintf('"sort" option cannot be used with $operations[%d]["%s"]', $i, $type));
434+
}
435+
416436
if (! is_bool($args[2]['upsert'])) {
417437
throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]["upsert"]', $i, $type), $args[2]['upsert'], 'boolean');
418438
}

src/Operation/ReplaceOne.php

+6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ class ReplaceOne implements Executable
7474
* Parameters can then be accessed as variables in an aggregate
7575
* expression context (e.g. "$$var").
7676
*
77+
* * sort (document): Determines which document the operation modifies if
78+
* the query selects multiple documents.
79+
*
80+
* This is not supported for server versions < 8.0 and will result in an
81+
* exception at execution time if used.
82+
*
7783
* * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
7884
*
7985
* @param string $databaseName Database name

src/Operation/Update.php

+12-2
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ public function __construct(private string $databaseName, private string $collec
149149
throw InvalidArgumentException::expectedDocumentType('"let" option', $options['let']);
150150
}
151151

152+
if (isset($options['sort']) && ! is_document($options['sort'])) {
153+
throw InvalidArgumentException::expectedDocumentType('"sort" option', $options['sort']);
154+
}
155+
156+
if (isset($options['sort']) && $options['multi']) {
157+
throw new InvalidArgumentException('"sort" option cannot be used with multi-document updates');
158+
}
159+
152160
if (isset($options['bypassDocumentValidation']) && ! $options['bypassDocumentValidation']) {
153161
unset($options['bypassDocumentValidation']);
154162
}
@@ -270,8 +278,10 @@ private function createUpdateOptions(): array
270278
}
271279
}
272280

273-
if (isset($this->options['collation'])) {
274-
$updateOptions['collation'] = (object) $this->options['collation'];
281+
foreach (['collation', 'sort'] as $option) {
282+
if (isset($this->options[$option])) {
283+
$updateOptions[$option] = (object) $this->options[$option];
284+
}
275285
}
276286

277287
return $updateOptions;

src/Operation/UpdateOne.php

+6
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ class UpdateOne implements Executable, Explainable
7272
* Parameters can then be accessed as variables in an aggregate
7373
* expression context (e.g. "$$var").
7474
*
75+
* * sort (document): Determines which document the operation modifies if
76+
* the query selects multiple documents.
77+
*
78+
* This is not supported for server versions < 8.0 and will result in an
79+
* exception at execution time if used.
80+
*
7581
* * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
7682
*
7783
* @param string $databaseName Database name

tests/Operation/BulkWriteTest.php

+29
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,16 @@ public function testReplaceOneCollationOptionTypeCheck($collation): void
217217
]);
218218
}
219219

220+
#[DataProvider('provideInvalidDocumentValues')]
221+
public function testReplaceOneSortOptionTypeCheck($sort): void
222+
{
223+
$this->expectException(InvalidArgumentException::class);
224+
$this->expectExceptionMessageMatches('/Expected \$operations\[0\]\["replaceOne"\]\[2\]\["sort"\] to have type "document" \(array or object\) but found ".+"/');
225+
new BulkWrite($this->getDatabaseName(), $this->getCollectionName(), [
226+
[BulkWrite::REPLACE_ONE => [['x' => 1], ['y' => 1], ['sort' => $sort]]],
227+
]);
228+
}
229+
220230
#[DataProvider('provideInvalidBooleanValues')]
221231
public function testReplaceOneUpsertOptionTypeCheck($upsert): void
222232
{
@@ -322,6 +332,15 @@ public function testUpdateManyCollationOptionTypeCheck($collation): void
322332
]);
323333
}
324334

335+
public function testUpdateManyProhibitsSortOption(): void
336+
{
337+
$this->expectException(InvalidArgumentException::class);
338+
$this->expectExceptionMessage('"sort" option cannot be used with $operations[0]["updateMany"]');
339+
new BulkWrite($this->getDatabaseName(), $this->getCollectionName(), [
340+
[BulkWrite::UPDATE_MANY => [['x' => 1], ['$set' => ['y' => 1]], ['sort' => ['z' => 1]]]],
341+
]);
342+
}
343+
325344
#[DataProvider('provideInvalidBooleanValues')]
326345
public function testUpdateManyUpsertOptionTypeCheck($upsert): void
327346
{
@@ -410,6 +429,16 @@ public function testUpdateOneCollationOptionTypeCheck($collation): void
410429
]);
411430
}
412431

432+
#[DataProvider('provideInvalidDocumentValues')]
433+
public function testUpdateOneSortOptionTypeCheck($sort): void
434+
{
435+
$this->expectException(InvalidArgumentException::class);
436+
$this->expectExceptionMessageMatches('/Expected \$operations\[0\]\["updateOne"\]\[2\]\["sort"\] to have type "document" \(array or object\) but found ".+"/');
437+
new BulkWrite($this->getDatabaseName(), $this->getCollectionName(), [
438+
[BulkWrite::UPDATE_ONE => [['x' => 1], ['$set' => ['y' => 1]], ['sort' => $sort]]],
439+
]);
440+
}
441+
413442
#[DataProvider('provideInvalidBooleanValues')]
414443
public function testUpdateOneUpsertOptionTypeCheck($upsert): void
415444
{

tests/Operation/UpdateTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public static function provideInvalidConstructorOptions()
4040
'collation' => self::getInvalidDocumentValues(),
4141
'hint' => self::getInvalidHintValues(),
4242
'multi' => self::getInvalidBooleanValues(),
43+
'sort' => self::getInvalidDocumentValues(),
4344
'session' => self::getInvalidSessionValues(),
4445
'upsert' => self::getInvalidBooleanValues(),
4546
'writeConcern' => self::getInvalidWriteConcernValues(),
@@ -55,6 +56,13 @@ public function testConstructorMultiOptionProhibitsReplacementDocumentOrEmptyPip
5556
new Update($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1], $update, ['multi' => true]);
5657
}
5758

59+
public function testConstructorMultiOptionProhibitsSortOption(): void
60+
{
61+
$this->expectException(InvalidArgumentException::class);
62+
$this->expectExceptionMessage('"sort" option cannot be used with multi-document updates');
63+
new Update($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1], ['$set' => ['x' => 2]], ['multi' => true, 'sort' => ['x' => 1]]);
64+
}
65+
5866
public function testExplainableCommandDocument(): void
5967
{
6068
$options = [

tests/UnifiedSpecTests/UnifiedSpecTest.php

-5
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ class UnifiedSpecTest extends FunctionalTestCase
3737
// mongoc_cluster_stream_for_server does not retry handshakes (CDRIVER-4532, PHPLIB-1033, PHPLIB-1042)
3838
'retryable-reads/retryable reads handshake failures' => 'Handshakes are not retried (CDRIVER-4532)',
3939
'retryable-writes/retryable writes handshake failures' => 'Handshakes are not retried (CDRIVER-4532)',
40-
// sort option for update operations is not supported (PHPLIB-1492)
41-
'crud/BulkWrite replaceOne-sort' => 'Sort for replace operations is not supported (PHPLIB-1492)',
42-
'crud/BulkWrite updateOne-sort' => 'Sort for update operations is not supported (PHPLIB-1492)',
43-
'crud/replaceOne-sort' => 'Sort for replace operations is not supported (PHPLIB-1492)',
44-
'crud/updateOne-sort' => 'Sort for update operations is not supported (PHPLIB-1492)',
4540
'crud/bypassDocumentValidation' => 'bypassDocumentValidation is handled by libmongoc (PHPLIB-1576)',
4641
];
4742

tests/UnifiedSpecTests/Util.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ final class Util
105105
'findOne' => ['let', 'filter', 'session', 'allowDiskUse', 'allowPartialResults', 'batchSize', 'collation', 'comment', 'cursorType', 'hint', 'max', 'maxAwaitTimeMS', 'maxScan', 'maxTimeMS', 'min', 'modifiers', 'noCursorTimeout', 'oplogReplay', 'projection', 'returnKey', 'showRecordId', 'skip', 'snapshot', 'sort'],
106106
'findOneAndReplace' => ['let', 'returnDocument', 'filter', 'replacement', 'session', 'projection', 'returnDocument', 'upsert', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'maxTimeMS', 'new', 'remove', 'sort', 'comment'],
107107
'rename' => ['to', 'comment', 'dropTarget'],
108-
'replaceOne' => ['let', 'filter', 'replacement', 'session', 'upsert', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'comment'],
108+
'replaceOne' => ['let', 'filter', 'replacement', 'session', 'upsert', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'comment', 'sort'],
109109
'findOneAndUpdate' => ['let', 'returnDocument', 'filter', 'update', 'session', 'upsert', 'projection', 'remove', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'maxTimeMS', 'sort', 'comment'],
110110
'updateMany' => ['let', 'filter', 'update', 'session', 'upsert', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'comment'],
111-
'updateOne' => ['let', 'filter', 'update', 'session', 'upsert', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'comment'],
111+
'updateOne' => ['let', 'filter', 'update', 'session', 'upsert', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'comment', 'sort'],
112112
'updateSearchIndex' => ['name', 'definition'],
113113
'insertMany' => ['documents', 'session', 'ordered', 'bypassDocumentValidation', 'comment'],
114114
'insertOne' => ['document', 'session', 'bypassDocumentValidation', 'comment'],

0 commit comments

Comments
 (0)