Skip to content

Commit 80c2906

Browse files
abnegateclaude
andcommitted
fix(sqlite): match ON CONFLICT column order to the actual UNIQUE index
getSQLIndex automatically prepends \`_tenant\` to every index under shared tables, so the resulting UNIQUE constraint on the documents table is (_tenant, _uid) rather than (_uid, _tenant). SQLite's ON CONFLICT clause requires the same column order — the upsert path now emits (_tenant, _uid) under shared tables and the underlying UNIQUE matches. Also drop the redundant `composite UNIQUE` createCollection edit since the auto-prepend already covers it for both the documents and permissions tables. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6c0ee2a commit 80c2906

1 file changed

Lines changed: 14 additions & 17 deletions

File tree

src/Database/Adapter/SQLite.php

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -210,25 +210,18 @@ public function createCollection(string $name, array $attributes = [], array $in
210210
->prepare($permissions)
211211
->execute();
212212

213-
// For shared tables the same `_uid` legitimately appears across
214-
// tenants — make the UNIQUE constraint composite so cross-tenant
215-
// documents don't collide and `ON CONFLICT(_uid, _tenant)` from
216-
// the upsert path has a matching index to land on.
217-
$uidColumns = $this->sharedTables ? ['_uid', '_tenant'] : ['_uid'];
218-
$this->createIndex($id, '_index1', Database::INDEX_UNIQUE, $uidColumns, [], []);
213+
// getSQLIndex automatically prepends `_tenant` to every index
214+
// when sharedTables is on, so the resulting UNIQUE here is
215+
// already composite on (_tenant, _uid) under shared tables.
216+
$this->createIndex($id, '_index1', Database::INDEX_UNIQUE, ['_uid'], [], []);
219217
$this->createIndex($id, '_created_at', Database::INDEX_KEY, [ '_createdAt'], [], []);
220218
$this->createIndex($id, '_updated_at', Database::INDEX_KEY, [ '_updatedAt'], [], []);
221219

222-
// Match MariaDB's shared shape: include _tenant in the perms
223-
// UNIQUE so two tenants can both grant the same role on the
224-
// same document without colliding. Without _tenant the
225-
// upsert path for shared tables hits a UNIQUE violation
226-
// when a new tenant re-inserts permissions for a row that
227-
// already exists in another tenant.
228-
$permsColumns = $this->sharedTables
229-
? ['_document', '_type', '_permission', '_tenant']
230-
: ['_document', '_type', '_permission'];
231-
$this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, $permsColumns, [], []);
220+
// getSQLIndex prepends `_tenant` automatically under shared
221+
// tables, so the resulting UNIQUE on the perms table is
222+
// (_tenant, _document, _type, _permission) and two tenants
223+
// can hold the same role on the same document.
224+
$this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], []);
232225
$this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission', '_type'], [], []);
233226

234227
if ($this->sharedTables) {
@@ -2111,7 +2104,11 @@ public function getUpsertStatement(
21112104
}
21122105
}
21132106

2114-
$conflictKeys = $this->sharedTables ? '(_uid, _tenant)' : '(_uid)';
2107+
// getSQLIndex prepends `_tenant` to every index column list
2108+
// under shared tables, so the actual UNIQUE on the documents
2109+
// table is (_tenant, _uid). SQLite's ON CONFLICT clause needs
2110+
// the same column order to match a UNIQUE constraint.
2111+
$conflictKeys = $this->sharedTables ? '(_tenant, _uid)' : '(_uid)';
21152112

21162113
$stmt = $this->getPDO()->prepare(
21172114
"

0 commit comments

Comments
 (0)