Skip to content

Commit 4f0be24

Browse files
authored
Merge pull request #792 from premtsd-code/feat-upsert-preserve-sequence
2 parents 53c674f + cf11809 commit 4f0be24

File tree

3 files changed

+178
-2
lines changed

3 files changed

+178
-2
lines changed

src/Database/Database.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,8 @@ class Database
408408

409409
protected bool $preserveDates = false;
410410

411+
protected bool $preserveSequence = false;
412+
411413
protected int $maxQueryValues = 5000;
412414

413415
protected bool $migrating = false;
@@ -1400,6 +1402,30 @@ public function withPreserveDates(callable $callback): mixed
14001402
}
14011403
}
14021404

1405+
public function getPreserveSequence(): bool
1406+
{
1407+
return $this->preserveSequence;
1408+
}
1409+
1410+
public function setPreserveSequence(bool $preserve): static
1411+
{
1412+
$this->preserveSequence = $preserve;
1413+
1414+
return $this;
1415+
}
1416+
1417+
public function withPreserveSequence(callable $callback): mixed
1418+
{
1419+
$previous = $this->preserveSequence;
1420+
$this->preserveSequence = true;
1421+
1422+
try {
1423+
return $callback();
1424+
} finally {
1425+
$this->preserveSequence = $previous;
1426+
}
1427+
}
1428+
14031429
public function setMaxQueryValues(int $max): self
14041430
{
14051431
$this->maxQueryValues = $max;
@@ -6594,8 +6620,11 @@ public function upsertDocumentsWithIncrease(
65946620
$document
65956621
->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId())
65966622
->setAttribute('$collection', $collection->getId())
6597-
->setAttribute('$updatedAt', ($updatedAt === null || !$this->preserveDates) ? $time : $updatedAt)
6598-
->removeAttribute('$sequence');
6623+
->setAttribute('$updatedAt', ($updatedAt === null || !$this->preserveDates) ? $time : $updatedAt);
6624+
6625+
if (!$this->preserveSequence) {
6626+
$document->removeAttribute('$sequence');
6627+
}
65996628

66006629
$createdAt = $document->getCreatedAt();
66016630
if ($createdAt === null || !$this->preserveDates) {

src/Database/Mirror.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ public function setPreserveDates(bool $preserve): static
139139
return $this;
140140
}
141141

142+
public function setPreserveSequence(bool $preserve): static
143+
{
144+
$this->delegate(__FUNCTION__, \func_get_args());
145+
146+
$this->preserveSequence = $preserve;
147+
148+
return $this;
149+
}
150+
142151
public function enableValidation(): static
143152
{
144153
$this->delegate(__FUNCTION__);

tests/e2e/Adapter/Scopes/DocumentTests.php

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,144 @@ public function testUpsertMixedPermissionDelta(): void
11781178
], $db->getDocument(__FUNCTION__, 'b')->getPermissions());
11791179
}
11801180

1181+
public function testPreserveSequenceUpsert(): void
1182+
{
1183+
/** @var Database $database */
1184+
$database = $this->getDatabase();
1185+
1186+
if (!$database->getAdapter()->getSupportForUpserts()) {
1187+
$this->expectNotToPerformAssertions();
1188+
return;
1189+
}
1190+
1191+
$collectionName = 'preserve_sequence_upsert';
1192+
1193+
$database->createCollection($collectionName);
1194+
1195+
if ($database->getAdapter()->getSupportForAttributes()) {
1196+
$database->createAttribute($collectionName, 'name', Database::VAR_STRING, 128, true);
1197+
}
1198+
1199+
// Create initial documents
1200+
$doc1 = $database->createDocument($collectionName, new Document([
1201+
'$id' => 'doc1',
1202+
'$permissions' => [
1203+
Permission::read(Role::any()),
1204+
Permission::update(Role::any()),
1205+
],
1206+
'name' => 'Alice',
1207+
]));
1208+
1209+
$doc2 = $database->createDocument($collectionName, new Document([
1210+
'$id' => 'doc2',
1211+
'$permissions' => [
1212+
Permission::read(Role::any()),
1213+
Permission::update(Role::any()),
1214+
],
1215+
'name' => 'Bob',
1216+
]));
1217+
1218+
$originalSeq1 = $doc1->getSequence();
1219+
$originalSeq2 = $doc2->getSequence();
1220+
1221+
$this->assertNotEmpty($originalSeq1);
1222+
$this->assertNotEmpty($originalSeq2);
1223+
1224+
// Test: Without preserveSequence (default), $sequence should be ignored
1225+
$database->setPreserveSequence(false);
1226+
1227+
$database->upsertDocuments($collectionName, [
1228+
new Document([
1229+
'$id' => 'doc1',
1230+
'$sequence' => 999, // Try to set a different sequence
1231+
'$permissions' => [
1232+
Permission::read(Role::any()),
1233+
Permission::update(Role::any()),
1234+
],
1235+
'name' => 'Alice Updated',
1236+
]),
1237+
]);
1238+
1239+
$doc1Updated = $database->getDocument($collectionName, 'doc1');
1240+
$this->assertEquals('Alice Updated', $doc1Updated->getAttribute('name'));
1241+
$this->assertEquals($originalSeq1, $doc1Updated->getSequence()); // Sequence unchanged
1242+
1243+
// Test: With preserveSequence=true, $sequence from document should be used
1244+
$database->setPreserveSequence(true);
1245+
1246+
$database->upsertDocuments($collectionName, [
1247+
new Document([
1248+
'$id' => 'doc2',
1249+
'$sequence' => $originalSeq2, // Keep original sequence
1250+
'$permissions' => [
1251+
Permission::read(Role::any()),
1252+
Permission::update(Role::any()),
1253+
],
1254+
'name' => 'Bob Updated',
1255+
]),
1256+
]);
1257+
1258+
$doc2Updated = $database->getDocument($collectionName, 'doc2');
1259+
$this->assertEquals('Bob Updated', $doc2Updated->getAttribute('name'));
1260+
$this->assertEquals($originalSeq2, $doc2Updated->getSequence()); // Sequence preserved
1261+
1262+
// Test: withPreserveSequence helper
1263+
$database->setPreserveSequence(false);
1264+
1265+
$doc1 = $database->getDocument($collectionName, 'doc1');
1266+
$currentSeq1 = $doc1->getSequence();
1267+
1268+
$database->withPreserveSequence(function () use ($database, $collectionName, $currentSeq1) {
1269+
$database->upsertDocuments($collectionName, [
1270+
new Document([
1271+
'$id' => 'doc1',
1272+
'$sequence' => $currentSeq1,
1273+
'$permissions' => [
1274+
Permission::read(Role::any()),
1275+
Permission::update(Role::any()),
1276+
],
1277+
'name' => 'Alice Final',
1278+
]),
1279+
]);
1280+
});
1281+
1282+
$doc1Final = $database->getDocument($collectionName, 'doc1');
1283+
$this->assertEquals('Alice Final', $doc1Final->getAttribute('name'));
1284+
$this->assertEquals($currentSeq1, $doc1Final->getSequence());
1285+
1286+
// Verify flag was reset after withPreserveSequence
1287+
$this->assertFalse($database->getPreserveSequence());
1288+
1289+
// Test: With preserveSequence=true, invalid $sequence should throw error (SQL adapters only)
1290+
$database->setPreserveSequence(true);
1291+
1292+
try {
1293+
$database->upsertDocuments($collectionName, [
1294+
new Document([
1295+
'$id' => 'doc1',
1296+
'$sequence' => 'abc', // Invalid sequence value
1297+
'$permissions' => [
1298+
Permission::read(Role::any()),
1299+
Permission::update(Role::any()),
1300+
],
1301+
'name' => 'Alice Invalid',
1302+
]),
1303+
]);
1304+
// Schemaless adapters may not validate sequence type, so only fail for schemaful
1305+
if ($database->getAdapter()->getSupportForAttributes()) {
1306+
$this->fail('Expected StructureException for invalid sequence');
1307+
}
1308+
} catch (Throwable $e) {
1309+
if ($database->getAdapter()->getSupportForAttributes()) {
1310+
$this->assertInstanceOf(StructureException::class, $e);
1311+
$this->assertStringContainsString('sequence', $e->getMessage());
1312+
}
1313+
}
1314+
1315+
$database->setPreserveSequence(false);
1316+
$database->deleteCollection($collectionName);
1317+
}
1318+
11811319
public function testRespectNulls(): Document
11821320
{
11831321
/** @var Database $database */

0 commit comments

Comments
 (0)