Skip to content

Commit b0a8392

Browse files
committed
Push skipDuplicates scope guard down to Adapter layer
- Add $skipDuplicates property + skipDuplicates() scope guard to Adapter - Remove bool $ignore parameter from all adapter createDocuments signatures (Adapter, Pool, Mongo, SQL) - Remove bool $ignore from SQL helper methods (getInsertKeyword, getInsertSuffix, getInsertPermissionsSuffix) and Postgres/SQLite overrides - Pool delegate() and withTransaction() propagate skipDuplicates state to pooled/pinned adapter via the scope guard - Database::createDocuments() wraps adapter call in adapter->skipDuplicates() when the flag is set, drops the local $ignore variable
1 parent 63d9902 commit b0a8392

7 files changed

Lines changed: 61 additions & 34 deletions

File tree

src/Database/Adapter.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,29 @@ abstract class Adapter
3333

3434
protected bool $alterLocks = false;
3535

36+
protected bool $skipDuplicates = false;
37+
38+
/**
39+
* Run a callback with skipDuplicates enabled.
40+
* Duplicate key errors during createDocuments() will be silently skipped
41+
* instead of thrown. Nestable — saves and restores previous state.
42+
*
43+
* @template T
44+
* @param callable(): T $callback
45+
* @return T
46+
*/
47+
public function skipDuplicates(callable $callback): mixed
48+
{
49+
$previous = $this->skipDuplicates;
50+
$this->skipDuplicates = true;
51+
52+
try {
53+
return $callback();
54+
} finally {
55+
$this->skipDuplicates = $previous;
56+
}
57+
}
58+
3659
/**
3760
* @var array<string, mixed>
3861
*/
@@ -729,13 +752,12 @@ abstract public function createDocument(Document $collection, Document $document
729752
*
730753
* @param Document $collection
731754
* @param array<Document> $documents
732-
* @param bool $ignore If true, silently ignore duplicate documents instead of throwing
733755
*
734756
* @return array<Document>
735757
*
736758
* @throws DatabaseException
737759
*/
738-
abstract public function createDocuments(Document $collection, array $documents, bool $ignore = false): array;
760+
abstract public function createDocuments(Document $collection, array $documents): array;
739761

740762
/**
741763
* Update Document

src/Database/Adapter/Mongo.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,7 +1460,7 @@ public function castingBefore(Document $collection, Document $document): Documen
14601460
* @throws DuplicateException
14611461
* @throws DatabaseException
14621462
*/
1463-
public function createDocuments(Document $collection, array $documents, bool $ignore = false): array
1463+
public function createDocuments(Document $collection, array $documents): array
14641464
{
14651465
$name = $this->getNamespace() . '_' . $this->filter($collection->getId());
14661466
$options = $this->getTransactionOptions();
@@ -1488,7 +1488,7 @@ public function createDocuments(Document $collection, array $documents, bool $ig
14881488
}
14891489

14901490
// Pre-filter duplicates within the session to avoid aborting the transaction.
1491-
if ($ignore && !empty($records)) {
1491+
if ($this->skipDuplicates && !empty($records)) {
14921492
$existingKeys = [];
14931493

14941494
try {

src/Database/Adapter/Pool.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public function __construct(UtopiaPool $pool)
4343
public function delegate(string $method, array $args): mixed
4444
{
4545
if ($this->pinnedAdapter !== null) {
46-
return $this->pinnedAdapter->{$method}(...$args);
46+
$invoke = fn () => $this->pinnedAdapter->{$method}(...$args);
47+
return $this->skipDuplicates ? $this->pinnedAdapter->skipDuplicates($invoke) : $invoke();
4748
}
4849

4950
return $this->pool->use(function (Adapter $adapter) use ($method, $args) {
@@ -66,7 +67,8 @@ public function delegate(string $method, array $args): mixed
6667
$adapter->setMetadata($key, $value);
6768
}
6869

69-
return $adapter->{$method}(...$args);
70+
$invoke = fn () => $adapter->{$method}(...$args);
71+
return $this->skipDuplicates ? $adapter->skipDuplicates($invoke) : $invoke();
7072
});
7173
}
7274

@@ -146,7 +148,8 @@ public function withTransaction(callable $callback): mixed
146148

147149
$this->pinnedAdapter = $adapter;
148150
try {
149-
return $adapter->withTransaction($callback);
151+
$invoke = fn () => $adapter->withTransaction($callback);
152+
return $this->skipDuplicates ? $adapter->skipDuplicates($invoke) : $invoke();
150153
} finally {
151154
$this->pinnedAdapter = null;
152155
}
@@ -268,7 +271,7 @@ public function createDocument(Document $collection, Document $document): Docume
268271
return $this->delegate(__FUNCTION__, \func_get_args());
269272
}
270273

271-
public function createDocuments(Document $collection, array $documents, bool $ignore = false): array
274+
public function createDocuments(Document $collection, array $documents): array
272275
{
273276
return $this->delegate(__FUNCTION__, \func_get_args());
274277
}

src/Database/Adapter/Postgres.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,14 +1365,14 @@ public function updateDocument(Document $collection, string $id, Document $docum
13651365
return $document;
13661366
}
13671367

1368-
protected function getInsertKeyword(bool $ignore): string
1368+
protected function getInsertKeyword(): string
13691369
{
13701370
return 'INSERT INTO';
13711371
}
13721372

1373-
protected function getInsertSuffix(bool $ignore, string $table): string
1373+
protected function getInsertSuffix(string $table): string
13741374
{
1375-
if (!$ignore) {
1375+
if (!$this->skipDuplicates) {
13761376
return '';
13771377
}
13781378

@@ -1381,9 +1381,9 @@ protected function getInsertSuffix(bool $ignore, string $table): string
13811381
return "ON CONFLICT {$conflictTarget} DO NOTHING";
13821382
}
13831383

1384-
protected function getInsertPermissionsSuffix(bool $ignore): string
1384+
protected function getInsertPermissionsSuffix(): string
13851385
{
1386-
if (!$ignore) {
1386+
if (!$this->skipDuplicates) {
13871387
return '';
13881388
}
13891389

src/Database/Adapter/SQL.php

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2471,14 +2471,14 @@ protected function execute(mixed $stmt): bool
24712471
* @throws DuplicateException
24722472
* @throws \Throwable
24732473
*/
2474-
public function createDocuments(Document $collection, array $documents, bool $ignore = false): array
2474+
public function createDocuments(Document $collection, array $documents): array
24752475
{
24762476
if (empty($documents)) {
24772477
return $documents;
24782478
}
24792479

24802480
// Pre-filter existing UIDs to prevent race-condition duplicates.
2481-
if ($ignore) {
2481+
if ($this->skipDuplicates) {
24822482
$collectionId = $collection->getId();
24832483
$name = $this->filter($collectionId);
24842484
$uids = \array_filter(\array_map(fn (Document $doc) => $doc->getId(), $documents), fn ($v) => $v !== null);
@@ -2649,9 +2649,9 @@ public function createDocuments(Document $collection, array $documents, bool $ig
26492649
$batchKeys = \implode(', ', $batchKeys);
26502650

26512651
$stmt = $this->getPDO()->prepare("
2652-
{$this->getInsertKeyword($ignore)} {$this->getSQLTable($name)} {$columns}
2652+
{$this->getInsertKeyword()} {$this->getSQLTable($name)} {$columns}
26532653
VALUES {$batchKeys}
2654-
{$this->getInsertSuffix($ignore, $name)}
2654+
{$this->getInsertSuffix($name)}
26552655
");
26562656

26572657
foreach ($bindValues as $key => $value) {
@@ -2661,7 +2661,7 @@ public function createDocuments(Document $collection, array $documents, bool $ig
26612661
$this->execute($stmt);
26622662

26632663
// Reconcile returned docs with actual inserts when a race condition skipped rows.
2664-
if ($ignore && $stmt->rowCount() < \count($documents)) {
2664+
if ($this->skipDuplicates && $stmt->rowCount() < \count($documents)) {
26652665
$expectedTimestamps = [];
26662666
foreach ($documents as $doc) {
26672667
$eKey = ($this->sharedTables && $this->tenantPerDocument)
@@ -2762,9 +2762,9 @@ public function createDocuments(Document $collection, array $documents, bool $ig
27622762
$permissions = \implode(', ', $permissions);
27632763

27642764
$sqlPermissions = "
2765-
{$this->getInsertKeyword($ignore)} {$this->getSQLTable($name . '_perms')} (_type, _permission, _document {$tenantColumn})
2765+
{$this->getInsertKeyword()} {$this->getSQLTable($name . '_perms')} (_type, _permission, _document {$tenantColumn})
27662766
VALUES {$permissions}
2767-
{$this->getInsertPermissionsSuffix($ignore)}
2767+
{$this->getInsertPermissionsSuffix()}
27682768
";
27692769

27702770
$stmtPermissions = $this->getPDO()->prepare($sqlPermissions);
@@ -2787,16 +2787,16 @@ public function createDocuments(Document $collection, array $documents, bool $ig
27872787
* Returns the INSERT keyword, optionally with IGNORE for duplicate handling.
27882788
* Override in adapter subclasses for DB-specific syntax.
27892789
*/
2790-
protected function getInsertKeyword(bool $ignore): string
2790+
protected function getInsertKeyword(): string
27912791
{
2792-
return $ignore ? 'INSERT IGNORE INTO' : 'INSERT INTO';
2792+
return $this->skipDuplicates ? 'INSERT IGNORE INTO' : 'INSERT INTO';
27932793
}
27942794

27952795
/**
27962796
* Returns a suffix appended after VALUES clause for duplicate handling.
27972797
* Override in adapter subclasses (e.g., Postgres uses ON CONFLICT DO NOTHING).
27982798
*/
2799-
protected function getInsertSuffix(bool $ignore, string $table): string
2799+
protected function getInsertSuffix(string $table): string
28002800
{
28012801
return '';
28022802
}
@@ -2805,7 +2805,7 @@ protected function getInsertSuffix(bool $ignore, string $table): string
28052805
* Returns a suffix for the permissions INSERT statement when ignoring duplicates.
28062806
* Override in adapter subclasses for DB-specific syntax.
28072807
*/
2808-
protected function getInsertPermissionsSuffix(bool $ignore): string
2808+
protected function getInsertPermissionsSuffix(): string
28092809
{
28102810
return '';
28112811
}

src/Database/Adapter/SQLite.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@
3434
*/
3535
class SQLite extends MariaDB
3636
{
37-
protected function getInsertKeyword(bool $ignore): string
37+
protected function getInsertKeyword(): string
3838
{
39-
return $ignore ? 'INSERT OR IGNORE INTO' : 'INSERT INTO';
39+
return $this->skipDuplicates ? 'INSERT OR IGNORE INTO' : 'INSERT INTO';
4040
}
4141

4242
/**

src/Database/Database.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5668,10 +5668,9 @@ public function createDocuments(
56685668
$modified = 0;
56695669

56705670
$tenantPerDocument = $this->adapter->getSharedTables() && $this->adapter->getTenantPerDocument();
5671-
$ignore = $this->skipDuplicates;
56725671

56735672
// Deduplicate intra-batch documents by ID (tenant-aware). First occurrence wins.
5674-
if ($ignore) {
5673+
if ($this->skipDuplicates) {
56755674
$seenIds = [];
56765675
$deduplicated = [];
56775676
foreach ($documents as $document) {
@@ -5692,7 +5691,7 @@ public function createDocuments(
56925691

56935692
// Pre-fetch existing IDs to skip relationship writes for known duplicates
56945693
$preExistingIds = [];
5695-
if ($ignore) {
5694+
if ($this->skipDuplicates) {
56965695
if ($tenantPerDocument) {
56975696
$idsByTenant = [];
56985697
foreach ($documents as $doc) {
@@ -5739,7 +5738,7 @@ public function createDocuments(
57395738
/** @var array<string, array<string, mixed>> $deferredRelationships */
57405739
$deferredRelationships = [];
57415740
$relationships = [];
5742-
if ($ignore && $this->resolveRelationships) {
5741+
if ($this->skipDuplicates && $this->resolveRelationships) {
57435742
$relationships = \array_filter($collection->getAttribute('attributes', []), fn ($attr) => $attr['type'] === self::VAR_RELATIONSHIP);
57445743
}
57455744

@@ -5813,7 +5812,7 @@ public function createDocuments(
58135812
}
58145813

58155814
foreach (\array_chunk($documents, $batchSize) as $chunk) {
5816-
if ($ignore && !empty($preExistingIds)) {
5815+
if ($this->skipDuplicates && !empty($preExistingIds)) {
58175816
$chunk = \array_values(\array_filter($chunk, function (Document $doc) use ($preExistingIds, $tenantPerDocument) {
58185817
$key = $tenantPerDocument
58195818
? $doc->getTenant() . ':' . $doc->getId()
@@ -5825,12 +5824,15 @@ public function createDocuments(
58255824
}
58265825
}
58275826

5828-
$batch = $this->withTransaction(function () use ($collection, $chunk, $ignore) {
5829-
return $this->adapter->createDocuments($collection, $chunk, $ignore);
5827+
$batch = $this->withTransaction(function () use ($collection, $chunk) {
5828+
$createFn = fn () => $this->adapter->createDocuments($collection, $chunk);
5829+
return $this->skipDuplicates
5830+
? $this->adapter->skipDuplicates($createFn)
5831+
: $createFn();
58305832
});
58315833

58325834
// Create deferred relationships only for docs that were actually inserted
5833-
if ($ignore && $this->resolveRelationships && \count($deferredRelationships) > 0) {
5835+
if ($this->skipDuplicates && $this->resolveRelationships && \count($deferredRelationships) > 0) {
58345836
foreach ($batch as $insertedDoc) {
58355837
$deferKey = $tenantPerDocument
58365838
? $insertedDoc->getTenant() . ':' . $insertedDoc->getId()

0 commit comments

Comments
 (0)