Skip to content

Commit 7ab1b83

Browse files
committed
(chore): resolve merge conflicts with main, keep query-lib adapter architecture
2 parents 3fade2e + 33e1b13 commit 7ab1b83

14 files changed

Lines changed: 489 additions & 25 deletions

File tree

.github/workflows/claude.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Claude
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, ready_for_review, reopened]
6+
pull_request_review:
7+
types: [submitted]
8+
issue_comment:
9+
types: [created]
10+
pull_request_review_comment:
11+
types: [created]
12+
issues:
13+
types: [opened, assigned]
14+
workflow_run:
15+
workflows: [Tests]
16+
types: [completed]
17+
18+
jobs:
19+
claude:
20+
# Caller must grant the union of every permission the callee's jobs ask
21+
# for; reusable workflows can't exceed the caller's ceiling.
22+
permissions:
23+
contents: write
24+
pull-requests: write
25+
issues: write
26+
actions: read
27+
id-token: write
28+
uses: abnegate/claude-pr-owner/.github/workflows/orchestrator.yml@e6baaab0ae24628a4d6e8b695b49a77f37e797f7 # v0.1.0
29+
secrets:
30+
oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
31+
with:
32+
improvement: true
33+
healing: true
34+
bots: true
35+
comments: true

.github/workflows/codeql-analysis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88

99
steps:
1010
- name: Checkout repository
11-
uses: actions/checkout@v4
11+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
1212
with:
1313
fetch-depth: 2
1414

.github/workflows/linter.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88

99
steps:
1010
- name: Checkout repository
11-
uses: actions/checkout@v4
11+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
1212
with:
1313
fetch-depth: 2
1414

.github/workflows/tests.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ jobs:
1616
runs-on: ubuntu-latest
1717
steps:
1818
- name: Checkout repository
19-
uses: actions/checkout@v4
19+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2020

2121
- name: Set up Docker Buildx
22-
uses: docker/setup-buildx-action@v3
22+
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
2323

2424
- name: Build Docker Image
25-
uses: docker/build-push-action@v3
25+
uses: docker/build-push-action@1104d471370f9806843c095c1db02b5a90c5f8b6 # v3.3.1
2626
with:
2727
context: .
2828
file: Dockerfile
@@ -34,7 +34,7 @@ jobs:
3434
outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
3535

3636
- name: Cache Docker Image
37-
uses: actions/cache@v3
37+
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0
3838
with:
3939
key: ${{ env.CACHE_KEY }}
4040
path: /tmp/${{ env.IMAGE }}.tar
@@ -46,10 +46,10 @@ jobs:
4646

4747
steps:
4848
- name: checkout
49-
uses: actions/checkout@v4
49+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
5050

5151
- name: Load Cache
52-
uses: actions/cache@v3
52+
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0
5353
with:
5454
key: ${{ env.CACHE_KEY }}
5555
path: /tmp/${{ env.IMAGE }}.tar
@@ -100,10 +100,10 @@ jobs:
100100

101101
steps:
102102
- name: checkout
103-
uses: actions/checkout@v4
103+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
104104

105105
- name: Load Cache
106-
uses: actions/cache@v3
106+
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0
107107
with:
108108
key: ${{ env.CACHE_KEY }}
109109
path: /tmp/${{ env.IMAGE }}.tar

src/Database/Adapter.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ abstract class Adapter implements Feature\Attributes, Feature\Collections, Featu
4646

4747
protected bool $alterLocks = false;
4848

49+
protected bool $skipDuplicates = false;
50+
4951
/**
5052
* @var array<string, mixed>
5153
*/
@@ -559,6 +561,27 @@ public function inTransaction(): bool
559561
return $this->inTransaction > 0;
560562
}
561563

564+
/**
565+
* Run a callback with skipDuplicates enabled.
566+
* Duplicate key errors during createDocuments() will be silently skipped
567+
* instead of thrown. Nestable — saves and restores previous state.
568+
*
569+
* @template T
570+
* @param callable(): T $callback
571+
* @return T
572+
*/
573+
public function skipDuplicates(callable $callback): mixed
574+
{
575+
$previous = $this->skipDuplicates;
576+
$this->skipDuplicates = true;
577+
578+
try {
579+
return $callback();
580+
} finally {
581+
$this->skipDuplicates = $previous;
582+
}
583+
}
584+
562585
/**
563586
* @template T
564587
*

src/Database/Adapter/Mongo.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,11 @@ public function withTransaction(callable $callback): mixed
421421
return $callback();
422422
}
423423

424+
// upsert + $setOnInsert hits WriteConflict (E112) under txn snapshot isolation.
425+
if ($this->skipDuplicates) {
426+
return $callback();
427+
}
428+
424429
try {
425430
$this->startTransaction();
426431
$result = $callback();
@@ -1396,6 +1401,42 @@ public function createDocuments(Document $collection, array $documents): array
13961401
$records[] = $record;
13971402
}
13981403

1404+
// insertMany aborts the txn on any duplicate; upsert + $setOnInsert no-ops instead.
1405+
if ($this->skipDuplicates) {
1406+
if (empty($records)) {
1407+
return [];
1408+
}
1409+
1410+
$operations = [];
1411+
foreach ($records as $record) {
1412+
$filter = ['_uid' => $record['_uid'] ?? ''];
1413+
if ($this->sharedTables) {
1414+
$filter['_tenant'] = $record['_tenant'] ?? $this->getTenant();
1415+
}
1416+
1417+
// Filter fields can't reappear in $setOnInsert (mongo path-conflict error).
1418+
$setOnInsert = $record;
1419+
unset($setOnInsert['_uid'], $setOnInsert['_tenant']);
1420+
1421+
if (empty($setOnInsert)) {
1422+
continue;
1423+
}
1424+
1425+
$operations[] = [
1426+
'filter' => $filter,
1427+
'update' => ['$setOnInsert' => $setOnInsert],
1428+
];
1429+
}
1430+
1431+
try {
1432+
$this->client->upsert($name, $operations, $options);
1433+
} catch (MongoException $e) {
1434+
throw $this->processException($e);
1435+
}
1436+
1437+
return $documents;
1438+
}
1439+
13991440
try {
14001441
$documents = $this->client->insertMany($name, $records, $options);
14011442
} catch (MongoException $e) {

src/Database/Adapter/Pool.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ public function __construct(UtopiaPool $pool)
5959
public function delegate(string $method, array $args): mixed
6060
{
6161
if ($this->pinnedAdapter !== null) {
62+
if ($this->skipDuplicates) {
63+
return $this->pinnedAdapter->skipDuplicates(
64+
fn () => $this->pinnedAdapter->{$method}(...$args)
65+
);
66+
}
6267
return $this->pinnedAdapter->{$method}(...$args);
6368
}
6469

@@ -94,6 +99,12 @@ public function delegate(string $method, array $args): mixed
9499
$adapter->addWriteHook($hook);
95100
}
96101
}
102+
103+
if ($this->skipDuplicates) {
104+
return $adapter->skipDuplicates(
105+
fn () => $adapter->{$method}(...$args)
106+
);
107+
}
97108
return $adapter->{$method}(...$args);
98109
});
99110
}
@@ -259,6 +270,11 @@ public function withTransaction(callable $callback): mixed
259270

260271
$this->pinnedAdapter = $adapter;
261272
try {
273+
if ($this->skipDuplicates) {
274+
return $adapter->skipDuplicates(
275+
fn () => $adapter->withTransaction($callback)
276+
);
277+
}
262278
return $adapter->withTransaction($callback);
263279
} finally {
264280
$this->pinnedAdapter = null;

src/Database/Adapter/Postgres.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,6 +1897,31 @@ protected function getIdentifierQuoteChar(): string
18971897
return '"';
18981898
}
18991899

1900+
protected function getInsertSuffix(string $table): string
1901+
{
1902+
if (! $this->skipDuplicates) {
1903+
return '';
1904+
}
1905+
1906+
$conflictTarget = $this->sharedTables ? '("_uid", "_tenant")' : '("_uid")';
1907+
1908+
return "ON CONFLICT {$conflictTarget} DO NOTHING";
1909+
}
1910+
1911+
protected function getInsertPermissionsSuffix(): string
1912+
{
1913+
if (! $this->skipDuplicates) {
1914+
return '';
1915+
}
1916+
1917+
$conflictTarget = $this->sharedTables
1918+
? '("_type", "_permission", "_document", "_tenant")'
1919+
: '("_type", "_permission", "_document")';
1920+
1921+
return "ON CONFLICT {$conflictTarget} DO NOTHING";
1922+
}
1923+
1924+
19001925
/**
19011926
* Get SQL expression for operator
19021927
*/

src/Database/Adapter/SQLite.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,4 +1828,9 @@ protected function executeUpsertBatch(
18281828
$stmt->execute();
18291829
$stmt->closeCursor();
18301830
}
1831+
1832+
protected function getInsertKeyword(): string
1833+
{
1834+
return $this->skipDuplicates ? 'INSERT OR IGNORE INTO' : 'INSERT INTO';
1835+
}
18311836
}

src/Database/Database.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,25 @@ public function withPreserveDates(callable $callback): mixed
955955
}
956956
}
957957

958+
/**
959+
* Execute a callback with skipDuplicates enabled, restoring the previous state afterward.
960+
*
961+
* @template T
962+
* @param callable(): T $callback
963+
* @return T
964+
*/
965+
public function skipDuplicates(callable $callback): mixed
966+
{
967+
$previous = $this->skipDuplicates;
968+
$this->skipDuplicates = true;
969+
970+
try {
971+
return $callback();
972+
} finally {
973+
$this->skipDuplicates = $previous;
974+
}
975+
}
976+
958977
/**
959978
* Set whether to preserve original sequence values instead of auto-generating them.
960979
*

0 commit comments

Comments
 (0)