@minnowdb/core 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/auto-store.d.ts +15 -3
- package/dist/engine/auto-store.js +48 -6
- package/dist/engine/client-audit-harness.js +123 -0
- package/dist/engine/client.js +21 -6
- package/dist/engine/database.js +223 -56
- package/dist/engine/worker-host.js +2 -0
- package/dist/engine/worker-server.js +4 -5
- package/dist/engine/worker-store-auto.js +2 -2
- package/dist/engine/write-coordinator.js +21 -1
- package/dist/storage/indexeddb-audit-helpers.js +269 -0
- package/dist/storage/indexeddb.js +107 -169
- package/dist/storage/opfs/coordination-helpers.js +54 -0
- package/dist/storage/opfs/index.d.ts +1 -1
- package/dist/storage/opfs/index.js +3 -2
- package/dist/storage/opfs/leader.js +44 -5
- package/dist/storage/opfs/power-loss-model.js +62 -0
- package/dist/storage/opfs/rpc.js +1 -0
- package/dist/storage/opfs/store.d.ts +12 -0
- package/dist/storage/opfs/store.js +59 -12
- package/dist/storage/toolkit/wal.js +16 -0
- package/dist/storage/toolkit/wire.js +3 -3
- package/dist/storage/types.d.ts +2 -2
- package/dist/transactions/index.d.ts +7 -0
- package/dist/transactions/index.js +17 -3
- package/package.json +1 -1
package/dist/engine/database.js
CHANGED
|
@@ -5226,6 +5226,7 @@ class MinnowDatabase {
|
|
|
5226
5226
|
tables: /* @__PURE__ */ new Map(),
|
|
5227
5227
|
mirrored: true,
|
|
5228
5228
|
generation: 0,
|
|
5229
|
+
flushedWork: 0,
|
|
5229
5230
|
flushing: Promise.resolve()
|
|
5230
5231
|
});
|
|
5231
5232
|
let closed = false;
|
|
@@ -5247,13 +5248,14 @@ class MinnowDatabase {
|
|
|
5247
5248
|
});
|
|
5248
5249
|
}
|
|
5249
5250
|
};
|
|
5251
|
+
const ownStagedWork = () => transaction.stagedWorkCount - (this.#scopeWrites.get(transaction)?.flushedWork ?? 0);
|
|
5250
5252
|
const guarded = async (run) => {
|
|
5251
|
-
const before =
|
|
5253
|
+
const before = ownStagedWork();
|
|
5252
5254
|
const generation = this.#scopeWriteGeneration(transaction);
|
|
5253
5255
|
try {
|
|
5254
5256
|
return await run();
|
|
5255
5257
|
} catch (error) {
|
|
5256
|
-
if (
|
|
5258
|
+
if (ownStagedWork() !== before || this.#scopeWriteGeneration(transaction) !== generation) {
|
|
5257
5259
|
poisoned ??= error;
|
|
5258
5260
|
}
|
|
5259
5261
|
throw error;
|
|
@@ -5262,7 +5264,7 @@ class MinnowDatabase {
|
|
|
5262
5264
|
const operations = {
|
|
5263
5265
|
query: async (sql, options2) => {
|
|
5264
5266
|
open();
|
|
5265
|
-
return this.#withReadReservation(() => this.#sessionQuery(transaction, sql, options2));
|
|
5267
|
+
return externalizeQueryResult(await this.#withReadReservation(() => this.#sessionQuery(transaction, sql, options2)));
|
|
5266
5268
|
},
|
|
5267
5269
|
execute: async (sql, params) => {
|
|
5268
5270
|
open();
|
|
@@ -5270,7 +5272,7 @@ class MinnowDatabase {
|
|
|
5270
5272
|
if (compiled.kind === "select") {
|
|
5271
5273
|
return this.#withReadReservation(async () => ({
|
|
5272
5274
|
kind: "rows",
|
|
5273
|
-
result: await this.#sessionQuery(transaction, compiled.sql, params === void 0 ? {} : { params })
|
|
5275
|
+
result: externalizeQueryResult(await this.#sessionQuery(transaction, compiled.sql, params === void 0 ? {} : { params }))
|
|
5274
5276
|
}));
|
|
5275
5277
|
}
|
|
5276
5278
|
const statement = bindStatementParameters(compiled, params);
|
|
@@ -5281,14 +5283,14 @@ class MinnowDatabase {
|
|
|
5281
5283
|
throw new TypeError(`${statement.kind.toUpperCase().replace("-", " ")} is not allowed inside a write scope`);
|
|
5282
5284
|
}
|
|
5283
5285
|
if (statement.kind === "update" || statement.kind === "delete") {
|
|
5284
|
-
const keyed = await this.#scopeKeyedMutation(statement);
|
|
5286
|
+
const keyed = await this.#scopeKeyedMutation(transaction, statement);
|
|
5285
5287
|
if (keyed !== void 0) {
|
|
5286
5288
|
return guarded(() => this.#runScopeKeyedMutation(transaction, keyed, () => {
|
|
5287
5289
|
staged += 1;
|
|
5288
5290
|
}));
|
|
5289
5291
|
}
|
|
5290
5292
|
}
|
|
5291
|
-
return writer.executeStatement(statement);
|
|
5293
|
+
return externalizeExecuteResult(await writer.executeStatement(statement));
|
|
5292
5294
|
},
|
|
5293
5295
|
insertBatch: async (tableName, input) => {
|
|
5294
5296
|
open();
|
|
@@ -5455,7 +5457,7 @@ class MinnowDatabase {
|
|
|
5455
5457
|
let point;
|
|
5456
5458
|
try {
|
|
5457
5459
|
pointReadTestHooks.attempted += 1;
|
|
5458
|
-
const table = await this.#findTable(shape.table);
|
|
5460
|
+
const table = await this.#findTable(shape.table, transaction);
|
|
5459
5461
|
point = await this.#withSessionVisibility(transaction, [table], options, (snapshot, visibility, realTables) => this.#pointReadAtSnapshot(shape, snapshot, realTables, visibility, options));
|
|
5460
5462
|
} catch (error) {
|
|
5461
5463
|
if (!(error instanceof UnknownTableError))
|
|
@@ -5478,7 +5480,7 @@ class MinnowDatabase {
|
|
|
5478
5480
|
options = this.#effectiveQueryOptions(options);
|
|
5479
5481
|
throwIfAborted(options.signal);
|
|
5480
5482
|
const names = collectRealTableNames(plan);
|
|
5481
|
-
const tables = await Promise.all(names.map((name) => this.#findTable(name)));
|
|
5483
|
+
const tables = await Promise.all(names.map((name) => this.#findTable(name, transaction)));
|
|
5482
5484
|
throwIfAborted(options.signal);
|
|
5483
5485
|
return this.#withSessionVisibility(transaction, tables, options, (snapshot, visibility, realTables) => this.#queryAtVisibility(plan, snapshot, visibility, realTables, true, options));
|
|
5484
5486
|
}
|
|
@@ -5775,6 +5777,7 @@ class MinnowDatabase {
|
|
|
5775
5777
|
await this.#settleScopeWriteSet(transaction, set);
|
|
5776
5778
|
}
|
|
5777
5779
|
async #settleScopeWriteSet(transaction, set) {
|
|
5780
|
+
await transaction.renewIfDue();
|
|
5778
5781
|
if (set.pendingRows >= this.#rowsPerBlock) {
|
|
5779
5782
|
await this.#flushScopeWriteSets(transaction, [set.table.id]);
|
|
5780
5783
|
return;
|
|
@@ -5802,11 +5805,14 @@ class MinnowDatabase {
|
|
|
5802
5805
|
const set = state.tables.get(tableId);
|
|
5803
5806
|
if (set === void 0 || set.pendingRows === 0)
|
|
5804
5807
|
continue;
|
|
5808
|
+
const before = transaction.stagedWorkCount;
|
|
5805
5809
|
try {
|
|
5806
5810
|
await this.#stageScopeWriteSet(transaction, state, set);
|
|
5807
5811
|
} catch (error) {
|
|
5808
5812
|
state.failure ??= error;
|
|
5809
5813
|
throw error;
|
|
5814
|
+
} finally {
|
|
5815
|
+
state.flushedWork += transaction.stagedWorkCount - before;
|
|
5810
5816
|
}
|
|
5811
5817
|
}
|
|
5812
5818
|
if (!state.mirrored)
|
|
@@ -5919,6 +5925,7 @@ class MinnowDatabase {
|
|
|
5919
5925
|
tables: /* @__PURE__ */ new Map(),
|
|
5920
5926
|
mirrored: false,
|
|
5921
5927
|
generation,
|
|
5928
|
+
flushedWork: 0,
|
|
5922
5929
|
flushing: Promise.resolve()
|
|
5923
5930
|
});
|
|
5924
5931
|
this.#stagedKeyOverlays.delete(transaction);
|
|
@@ -5960,8 +5967,33 @@ class MinnowDatabase {
|
|
|
5960
5967
|
answered.set(token, row);
|
|
5961
5968
|
}
|
|
5962
5969
|
if (committed.length > 0 && transaction.snapshotVersion !== null) {
|
|
5963
|
-
|
|
5964
|
-
|
|
5970
|
+
const pointRows = await this.#scopeCommittedRowsByKey(transaction, table, keyColumn, committed, names);
|
|
5971
|
+
const windows = [];
|
|
5972
|
+
if (pointRows === void 0) {
|
|
5973
|
+
for (let start = 0; start < committed.length; start += SCOPE_KEY_LOOKUP_WINDOW) {
|
|
5974
|
+
windows.push(committed.slice(start, start + SCOPE_KEY_LOOKUP_WINDOW));
|
|
5975
|
+
}
|
|
5976
|
+
}
|
|
5977
|
+
const patched = (rows) => {
|
|
5978
|
+
for (const row of rows) {
|
|
5979
|
+
const key = row[keyColumn.name] ?? null;
|
|
5980
|
+
if (key === null)
|
|
5981
|
+
continue;
|
|
5982
|
+
const token = keyToken(keyColumn.type, key);
|
|
5983
|
+
const patch = patches.get(token);
|
|
5984
|
+
if (patch !== void 0) {
|
|
5985
|
+
table.columns.forEach((column, position) => {
|
|
5986
|
+
const value = patch.values[position];
|
|
5987
|
+
if (value !== void 0 && wanted.has(column.name))
|
|
5988
|
+
row[column.name] = value;
|
|
5989
|
+
});
|
|
5990
|
+
}
|
|
5991
|
+
answered.set(token, row);
|
|
5992
|
+
}
|
|
5993
|
+
};
|
|
5994
|
+
if (pointRows !== void 0)
|
|
5995
|
+
patched(pointRows);
|
|
5996
|
+
for (const window of windows) {
|
|
5965
5997
|
const plan = {
|
|
5966
5998
|
sql: "(scope keyed lookup)",
|
|
5967
5999
|
base: { table: table.name, alias: table.name },
|
|
@@ -5985,25 +6017,39 @@ class MinnowDatabase {
|
|
|
5985
6017
|
version: transaction.snapshotVersion,
|
|
5986
6018
|
memoize: false
|
|
5987
6019
|
});
|
|
5988
|
-
|
|
5989
|
-
const key = row[keyColumn.name] ?? null;
|
|
5990
|
-
if (key === null)
|
|
5991
|
-
continue;
|
|
5992
|
-
const token = keyToken(keyColumn.type, key);
|
|
5993
|
-
const patch = patches.get(token);
|
|
5994
|
-
if (patch !== void 0) {
|
|
5995
|
-
table.columns.forEach((column, position) => {
|
|
5996
|
-
const value = patch.values[position];
|
|
5997
|
-
if (value !== void 0 && wanted.has(column.name))
|
|
5998
|
-
row[column.name] = value;
|
|
5999
|
-
});
|
|
6000
|
-
}
|
|
6001
|
-
answered.set(token, row);
|
|
6002
|
-
}
|
|
6020
|
+
patched(result.rows);
|
|
6003
6021
|
}
|
|
6004
6022
|
}
|
|
6005
6023
|
return [...answered.values()];
|
|
6006
6024
|
}
|
|
6025
|
+
async #scopeCommittedRowsByKey(transaction, table, keyColumn, keys, names) {
|
|
6026
|
+
if (pointReadTestHooks.disabled)
|
|
6027
|
+
return void 0;
|
|
6028
|
+
const values = [];
|
|
6029
|
+
for (const key of keys) {
|
|
6030
|
+
if (typeof key !== "number" && typeof key !== "string" && typeof key !== "boolean" && !(key instanceof Date)) {
|
|
6031
|
+
return void 0;
|
|
6032
|
+
}
|
|
6033
|
+
values.push(key);
|
|
6034
|
+
}
|
|
6035
|
+
const { segments, records } = await this.#scopeCommittedSegments(transaction, table);
|
|
6036
|
+
const visibility = {
|
|
6037
|
+
transactions: new Map(records.map((record) => [record.id, record])),
|
|
6038
|
+
segmentsByTable: /* @__PURE__ */ new Map([[table.id, segments]])
|
|
6039
|
+
};
|
|
6040
|
+
const realTables = /* @__PURE__ */ new Map([[table.name, table]]);
|
|
6041
|
+
const select = names.map((name) => ({ column: name, alias: name }));
|
|
6042
|
+
return this.#withLeasedSnapshot(transaction.snapshotVersion, async (snapshot) => {
|
|
6043
|
+
const rows = [];
|
|
6044
|
+
for (const value of values) {
|
|
6045
|
+
const result = await this.#pointReadAtSnapshot({ table: table.name, equalities: [{ column: keyColumn.name, value }], select }, snapshot, realTables, visibility, {});
|
|
6046
|
+
if (result === void 0)
|
|
6047
|
+
return void 0;
|
|
6048
|
+
rows.push(...result.rows);
|
|
6049
|
+
}
|
|
6050
|
+
return rows;
|
|
6051
|
+
});
|
|
6052
|
+
}
|
|
6007
6053
|
#scopeKeyLookup(transaction, table, keyColumn) {
|
|
6008
6054
|
return async (keys, projection) => {
|
|
6009
6055
|
const direct = await this.#scopeRowsByKey(transaction, table, keyColumn, keys, projection);
|
|
@@ -6021,11 +6067,11 @@ class MinnowDatabase {
|
|
|
6021
6067
|
return rows;
|
|
6022
6068
|
};
|
|
6023
6069
|
}
|
|
6024
|
-
async #scopeKeyedMutation(statement) {
|
|
6070
|
+
async #scopeKeyedMutation(transaction, statement) {
|
|
6025
6071
|
if (statement.from !== void 0 || statement.returning !== void 0 || statement.returningItems !== void 0 || statement.predicates.length !== 1) {
|
|
6026
6072
|
return void 0;
|
|
6027
6073
|
}
|
|
6028
|
-
const table = await this.#findTable(statement.table);
|
|
6074
|
+
const table = await this.#findTable(statement.table, transaction);
|
|
6029
6075
|
const keyColumn = getUniqueKeyColumn(table);
|
|
6030
6076
|
if (keyColumn === void 0 || keyColumn.hidden === true || keyColumn.sqlDomain !== void 0) {
|
|
6031
6077
|
return void 0;
|
|
@@ -6084,6 +6130,88 @@ class MinnowDatabase {
|
|
|
6084
6130
|
return void 0;
|
|
6085
6131
|
return { kind: "update", table, keyColumn, keys, changes };
|
|
6086
6132
|
}
|
|
6133
|
+
async #assertInsertKeysAbsent(transaction, table, batch, keys) {
|
|
6134
|
+
const inScope = this.#scopeWrites.has(transaction);
|
|
6135
|
+
const keyColumn = getUniqueKeyColumn(table);
|
|
6136
|
+
if (keys !== void 0 && keyColumn !== void 0) {
|
|
6137
|
+
const present = inScope ? await this.#scopeKeyPresence(transaction, table, keyColumn, [...keys.values()]) : this.#stagedKeyOverlay(transaction, table.id).added;
|
|
6138
|
+
for (const [token, value] of keys) {
|
|
6139
|
+
if (present.has(token)) {
|
|
6140
|
+
throw new UniqueConstraintError(table.name, publicKeyName(table, keyColumn), value);
|
|
6141
|
+
}
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
6144
|
+
if (!inScope)
|
|
6145
|
+
return;
|
|
6146
|
+
const rowCount = batch.rowCount ?? Object.values(batch.columns)[0]?.length ?? 0;
|
|
6147
|
+
for (const { indexId, index, columns } of readyUniqueSecondaryIndexes(table)) {
|
|
6148
|
+
const namespaceId = secondaryUniqueKeyNamespace(table.id, indexId);
|
|
6149
|
+
const terms = [];
|
|
6150
|
+
for (let row = 0; row < rowCount; row += 1) {
|
|
6151
|
+
const term = secondaryUniqueTerm(index, columns, columns.map((column) => batch.columns[column.name]?.[row] ?? null));
|
|
6152
|
+
if (term !== void 0)
|
|
6153
|
+
terms.push(term);
|
|
6154
|
+
}
|
|
6155
|
+
assertNoDuplicateUniqueTerms(index, terms);
|
|
6156
|
+
const overlay = this.#stagedKeyOverlay(transaction, namespaceId);
|
|
6157
|
+
const unresolved = [];
|
|
6158
|
+
for (const term of terms) {
|
|
6159
|
+
if (overlay.added.has(term)) {
|
|
6160
|
+
throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, term));
|
|
6161
|
+
}
|
|
6162
|
+
if (!overlay.removed.has(term))
|
|
6163
|
+
unresolved.push(term);
|
|
6164
|
+
}
|
|
6165
|
+
if (unresolved.length === 0)
|
|
6166
|
+
continue;
|
|
6167
|
+
const [existing] = await this.#existingUniqueKeysWindowed(namespaceId, unresolved);
|
|
6168
|
+
if (existing !== void 0) {
|
|
6169
|
+
throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, existing));
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
}
|
|
6173
|
+
async #assertUpdateUniqueTermsAbsent(transaction, table, input, preImages) {
|
|
6174
|
+
for (const { indexId, index, columns } of readyUniqueSecondaryIndexes(table)) {
|
|
6175
|
+
if (!columns.some((column) => input.changes[column.name] !== void 0))
|
|
6176
|
+
continue;
|
|
6177
|
+
const namespaceId = secondaryUniqueKeyNamespace(table.id, indexId);
|
|
6178
|
+
const released = /* @__PURE__ */ new Set();
|
|
6179
|
+
const taken = [];
|
|
6180
|
+
preImages.forEach((old, row) => {
|
|
6181
|
+
if (old === void 0)
|
|
6182
|
+
return;
|
|
6183
|
+
const before = secondaryUniqueTerm(index, columns, columns.map((column) => old[column.name] ?? null));
|
|
6184
|
+
const after = secondaryUniqueTerm(index, columns, columns.map((column) => {
|
|
6185
|
+
const assigned = input.changes[column.name];
|
|
6186
|
+
return assigned === void 0 ? old[column.name] ?? null : assigned[row] ?? null;
|
|
6187
|
+
}));
|
|
6188
|
+
if (before === after)
|
|
6189
|
+
return;
|
|
6190
|
+
if (before !== void 0)
|
|
6191
|
+
released.add(before);
|
|
6192
|
+
if (after !== void 0)
|
|
6193
|
+
taken.push(after);
|
|
6194
|
+
});
|
|
6195
|
+
assertNoDuplicateUniqueTerms(index, taken);
|
|
6196
|
+
const overlay = this.#stagedKeyOverlay(transaction, namespaceId);
|
|
6197
|
+
const unresolved = [];
|
|
6198
|
+
for (const term of taken) {
|
|
6199
|
+
if (released.has(term))
|
|
6200
|
+
continue;
|
|
6201
|
+
if (overlay.added.has(term)) {
|
|
6202
|
+
throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, term));
|
|
6203
|
+
}
|
|
6204
|
+
if (!overlay.removed.has(term))
|
|
6205
|
+
unresolved.push(term);
|
|
6206
|
+
}
|
|
6207
|
+
if (unresolved.length === 0)
|
|
6208
|
+
continue;
|
|
6209
|
+
const [existing] = await this.#existingUniqueKeysWindowed(namespaceId, unresolved);
|
|
6210
|
+
if (existing !== void 0) {
|
|
6211
|
+
throw await this.#translateUniqueConflict(new UniqueKeyConflictError(namespaceId, existing));
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6214
|
+
}
|
|
6087
6215
|
async #scopeKeyPresence(transaction, table, keyColumn, keys) {
|
|
6088
6216
|
const overlay = this.#stagedKeyOverlay(transaction, table.id);
|
|
6089
6217
|
const present = /* @__PURE__ */ new Set();
|
|
@@ -6122,7 +6250,7 @@ class MinnowDatabase {
|
|
|
6122
6250
|
return { kind: "delete", table: table.name, rowCount: deleted.rowCount };
|
|
6123
6251
|
}
|
|
6124
6252
|
const changes = Object.fromEntries(Object.entries(keyed.changes).map(([name, value]) => [name, present.map(() => value)]));
|
|
6125
|
-
const updated = await this.#sessionUpdate(transaction, table.name, { keys: present, changes });
|
|
6253
|
+
const updated = await this.#sessionUpdate(transaction, table.name, { keys: present, changes }, 1, true);
|
|
6126
6254
|
return { kind: "update", table: table.name, rowCount: updated.rowCount };
|
|
6127
6255
|
}
|
|
6128
6256
|
async #sessionInsert(transaction, tableName, input, kind, options, cascadeBudget = 1) {
|
|
@@ -6170,24 +6298,19 @@ class MinnowDatabase {
|
|
|
6170
6298
|
}
|
|
6171
6299
|
}
|
|
6172
6300
|
await this.#assertCompactionCapacity(table, transaction);
|
|
6301
|
+
await this.#assertForeignKeysPresent(table, (column) => batch.columns[column] ?? [], (sql, params) => this.#sessionQuery(transaction, sql, { params }), transaction);
|
|
6173
6302
|
const keys = batchKeys(table, batch);
|
|
6303
|
+
if (kind === "insert")
|
|
6304
|
+
await this.#assertInsertKeysAbsent(transaction, table, batch, keys);
|
|
6305
|
+
else
|
|
6306
|
+
assertBatchSecondaryTermsDistinct(table, batch);
|
|
6174
6307
|
if (keys !== void 0) {
|
|
6175
|
-
if (kind === "insert") {
|
|
6176
|
-
const keyColumn = getUniqueKeyColumn(table);
|
|
6177
|
-
const overlay = this.#stagedKeyOverlay(transaction, table.id);
|
|
6178
|
-
for (const [token, value] of keys) {
|
|
6179
|
-
if (overlay.added.has(token) && keyColumn !== void 0) {
|
|
6180
|
-
throw new UniqueConstraintError(table.name, keyColumn.name, value);
|
|
6181
|
-
}
|
|
6182
|
-
}
|
|
6183
|
-
}
|
|
6184
6308
|
transaction.setUniqueKeyChanges({
|
|
6185
6309
|
tableId: table.id,
|
|
6186
6310
|
keyTokens: [...keys.keys()],
|
|
6187
6311
|
requireAbsent: kind === "insert"
|
|
6188
6312
|
});
|
|
6189
6313
|
}
|
|
6190
|
-
await this.#assertForeignKeysPresent(table, (column) => batch.columns[column] ?? [], (sql, params) => this.#sessionQuery(transaction, sql, { params }), transaction);
|
|
6191
6314
|
const insertValueAt = (source, column, rowIndex) => source === "new" ? batch.columns[column]?.[rowIndex] ?? null : null;
|
|
6192
6315
|
stageSecondaryUniqueInsertChanges(transaction, table, batch, kind === "upsert" ? sessionUpsertFirings?.oldImages : void 0);
|
|
6193
6316
|
const buffered = this.#scopeWrites.has(transaction) && rowCount < SCOPE_DIRECT_STAGE_ROWS && !(table.triggers ?? []).some((trigger) => trigger.event === "insert" || trigger.event === "update");
|
|
@@ -6224,8 +6347,8 @@ class MinnowDatabase {
|
|
|
6224
6347
|
...generated.size === 0 ? {} : { generatedColumns: Object.fromEntries(generated) }
|
|
6225
6348
|
};
|
|
6226
6349
|
}
|
|
6227
|
-
async #sessionUpdate(transaction, tableName, input, cascadeBudget = 1) {
|
|
6228
|
-
const table = await this.#findTable(tableName);
|
|
6350
|
+
async #sessionUpdate(transaction, tableName, input, cascadeBudget = 1, keysVerified = false) {
|
|
6351
|
+
const table = await this.#findTable(tableName, transaction);
|
|
6229
6352
|
await this.#assertCompactionCapacity(table, transaction);
|
|
6230
6353
|
const keyColumn = getUniqueKeyColumn(table);
|
|
6231
6354
|
if (keyColumn === void 0) {
|
|
@@ -6235,14 +6358,16 @@ class MinnowDatabase {
|
|
|
6235
6358
|
rejectGeneratedUpdateAssignments(table, input);
|
|
6236
6359
|
const keys = validateUpdateBatch(table, keyColumn, input);
|
|
6237
6360
|
const overlay = this.#stagedKeyOverlay(transaction, table.id);
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6361
|
+
if (!keysVerified) {
|
|
6362
|
+
for (const [token, value] of keys) {
|
|
6363
|
+
if (overlay.removed.has(token)) {
|
|
6364
|
+
throw new MissingKeyError(table.name, keyColumn.name, value);
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6367
|
+
const committedKeys = new Map([...keys].filter(([token]) => !overlay.added.has(token)));
|
|
6368
|
+
if (committedKeys.size > 0) {
|
|
6369
|
+
await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
|
|
6241
6370
|
}
|
|
6242
|
-
}
|
|
6243
|
-
const committedKeys = new Map([...keys].filter(([token]) => !overlay.added.has(token)));
|
|
6244
|
-
if (committedKeys.size > 0) {
|
|
6245
|
-
await this.#assertKeysExist(table, keyColumn, transaction.snapshotVersion, committedKeys);
|
|
6246
6371
|
}
|
|
6247
6372
|
const sessionChecks = table.checks ?? [];
|
|
6248
6373
|
let changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false && foreignKeyColumns(key).some((column) => input.changes[column] !== void 0));
|
|
@@ -6250,6 +6375,9 @@ class MinnowDatabase {
|
|
|
6250
6375
|
input = applyStoredGeneratedUpdateChanges(table, input, preImages);
|
|
6251
6376
|
validateUpdateBatch(table, keyColumn, input);
|
|
6252
6377
|
changedForeignKey = (table.foreignKeys ?? []).some((key) => key.enforced !== false && foreignKeyColumns(key).some((column) => input.changes[column] !== void 0));
|
|
6378
|
+
if (this.#scopeWrites.has(transaction)) {
|
|
6379
|
+
await this.#assertUpdateUniqueTermsAbsent(transaction, table, input, preImages);
|
|
6380
|
+
}
|
|
6253
6381
|
stageSecondaryUniqueMutationChanges(transaction, table, input, preImages);
|
|
6254
6382
|
const secondaryDeltas = buildSecondaryUpdateDeltas(table, input, preImages);
|
|
6255
6383
|
if (secondaryDeltas.length > 0) {
|
|
@@ -6298,7 +6426,7 @@ class MinnowDatabase {
|
|
|
6298
6426
|
};
|
|
6299
6427
|
}
|
|
6300
6428
|
async #sessionDelete(transaction, tableName, input, cascadeBudget = 1, referentialBudget = REFERENTIAL_CASCADES) {
|
|
6301
|
-
const table = await this.#findTable(tableName);
|
|
6429
|
+
const table = await this.#findTable(tableName, transaction);
|
|
6302
6430
|
await this.#assertCompactionCapacity(table, transaction);
|
|
6303
6431
|
const keyColumn = getUniqueKeyColumn(table);
|
|
6304
6432
|
if (keyColumn === void 0) {
|
|
@@ -8404,9 +8532,7 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
|
|
|
8404
8532
|
return void 0;
|
|
8405
8533
|
const visibleBaseSegments = visibleByTable.get(freshBaseTable.name) ?? await this.#visibleSegmentRecords(freshBaseTable, snapshot, visibility);
|
|
8406
8534
|
this.#maybeScheduleAutoCompaction(freshBaseTable, visibleBaseSegments);
|
|
8407
|
-
const
|
|
8408
|
-
throwIfAborted(options.signal);
|
|
8409
|
-
const indexed = await this.#secondaryIndexPrunedSegments(freshBaseTable, ftsSegments, plan, snapshot);
|
|
8535
|
+
const indexed = await this.#indexPrunedSegments(freshBaseTable, visibleBaseSegments, plan, snapshot, visibility);
|
|
8410
8536
|
throwIfAborted(options.signal);
|
|
8411
8537
|
const baseSegments = indexed.segments;
|
|
8412
8538
|
const zonePruned = indexed.rows === void 0 ? await this.#zonePrunedStreamSegments(plan, freshBaseTable, projectedBaseColumns, baseSegments, snapshot) : void 0;
|
|
@@ -13323,6 +13449,19 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
|
|
|
13323
13449
|
}
|
|
13324
13450
|
return surviving;
|
|
13325
13451
|
}
|
|
13452
|
+
async #indexPrunedSegments(table, segments, plan, snapshot, visibility) {
|
|
13453
|
+
const own = overlayOwnedSegments(segments, visibility);
|
|
13454
|
+
if (own.length === 0) {
|
|
13455
|
+
const ftsSegments2 = await this.#ftsPrunedSegments(table, segments, plan, snapshot);
|
|
13456
|
+
return this.#secondaryIndexPrunedSegments(table, ftsSegments2, plan, snapshot);
|
|
13457
|
+
}
|
|
13458
|
+
if (own.some((segment) => segment.kind !== "insert"))
|
|
13459
|
+
return { segments, pruned: false };
|
|
13460
|
+
const committed = segments.filter((segment) => segment.transactionId !== visibility?.overlayTransactionId);
|
|
13461
|
+
const ftsSegments = await this.#ftsPrunedSegments(table, committed, plan, snapshot);
|
|
13462
|
+
const indexed = await this.#secondaryIndexPrunedSegments(table, ftsSegments, plan, snapshot);
|
|
13463
|
+
return { segments: [...indexed.segments, ...own], pruned: indexed.pruned };
|
|
13464
|
+
}
|
|
13326
13465
|
async #secondaryIndexPrunedSegments(table, segments, plan, snapshot) {
|
|
13327
13466
|
const predicates = secondaryIndexPredicates(plan, table);
|
|
13328
13467
|
if (predicates.length === 0) {
|
|
@@ -13598,13 +13737,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
|
|
|
13598
13737
|
}
|
|
13599
13738
|
async #materializeColumnarTableAtSnapshot(table, snapshot, projectedColumns, visibility, plan) {
|
|
13600
13739
|
const visibleSegments = await this.#visibleSegmentRecords(table, snapshot, visibility);
|
|
13601
|
-
if (plan !== void 0) {
|
|
13740
|
+
if (plan !== void 0 && overlayOwnedSegments(visibleSegments, visibility).length === 0) {
|
|
13602
13741
|
const covering = await this.#secondaryIndexCoveringTable(table, projectedColumns, visibleSegments, plan, snapshot);
|
|
13603
13742
|
if (covering !== void 0)
|
|
13604
13743
|
return covering;
|
|
13605
13744
|
}
|
|
13606
|
-
const
|
|
13607
|
-
const indexed = plan === void 0 ? { segments: ftsSegments, pruned: false } : await this.#secondaryIndexPrunedSegments(table, ftsSegments, plan, snapshot);
|
|
13745
|
+
const indexed = plan === void 0 ? { segments: visibleSegments, pruned: false } : await this.#indexPrunedSegments(table, visibleSegments, plan, snapshot, visibility);
|
|
13608
13746
|
const segments = indexed.segments;
|
|
13609
13747
|
const keyColumn = getUniqueKeyColumn(table);
|
|
13610
13748
|
if (segments.every((segment) => {
|
|
@@ -14614,13 +14752,23 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
|
|
|
14614
14752
|
this.#gzipVerdicts.delete(columnId);
|
|
14615
14753
|
return bytes;
|
|
14616
14754
|
}
|
|
14617
|
-
|
|
14755
|
+
#scopeTableRecords = /* @__PURE__ */ new WeakMap();
|
|
14756
|
+
async #findTable(name, scope) {
|
|
14618
14757
|
validateName(name, "Table");
|
|
14758
|
+
const cache = scope === void 0 || !this.#scopeWrites.has(scope) ? void 0 : this.#scopeTableRecords.get(scope) ?? (() => {
|
|
14759
|
+
const created = /* @__PURE__ */ new Map();
|
|
14760
|
+
this.#scopeTableRecords.set(scope, created);
|
|
14761
|
+
return created;
|
|
14762
|
+
})();
|
|
14763
|
+
const cached = cache?.get(name);
|
|
14764
|
+
if (cached !== void 0)
|
|
14765
|
+
return cached;
|
|
14619
14766
|
const table = await this.store.getTableByName(name) ?? await this.store.getTableByName(await this.#foldTableName(name));
|
|
14620
14767
|
if (table === void 0)
|
|
14621
14768
|
throw new UnknownTableError(name);
|
|
14622
14769
|
if (table.view !== void 0)
|
|
14623
14770
|
throw new TypeError(`${name} is a view, not a table`);
|
|
14771
|
+
cache?.set(name, table);
|
|
14624
14772
|
return table;
|
|
14625
14773
|
}
|
|
14626
14774
|
async #assertVisibleSegmentCursorTable(tableName, capturedTableId) {
|
|
@@ -15707,6 +15855,18 @@ function assertNoDuplicateUniqueTerms(index, terms) {
|
|
|
15707
15855
|
seen.add(term);
|
|
15708
15856
|
}
|
|
15709
15857
|
}
|
|
15858
|
+
function assertBatchSecondaryTermsDistinct(table, input) {
|
|
15859
|
+
const rowCount = input.rowCount ?? Object.values(input.columns)[0]?.length ?? 0;
|
|
15860
|
+
for (const { index, columns } of readyUniqueSecondaryIndexes(table)) {
|
|
15861
|
+
const terms = [];
|
|
15862
|
+
for (let row = 0; row < rowCount; row += 1) {
|
|
15863
|
+
const term = secondaryUniqueTerm(index, columns, columns.map((column) => input.columns[column.name]?.[row] ?? null));
|
|
15864
|
+
if (term !== void 0)
|
|
15865
|
+
terms.push(term);
|
|
15866
|
+
}
|
|
15867
|
+
assertNoDuplicateUniqueTerms(index, terms);
|
|
15868
|
+
}
|
|
15869
|
+
}
|
|
15710
15870
|
function stageSecondaryUniqueInsertChanges(transaction, table, input, oldImages) {
|
|
15711
15871
|
const rowCount = input.rowCount ?? Object.values(input.columns)[0]?.length ?? 0;
|
|
15712
15872
|
for (const { indexId, index, columns } of readyUniqueSecondaryIndexes(table)) {
|
|
@@ -15759,7 +15919,10 @@ function stageSecondaryUniqueMutationChanges(transaction, table, input, oldImage
|
|
|
15759
15919
|
const added = oldImages.flatMap((old, row) => {
|
|
15760
15920
|
if (old === void 0)
|
|
15761
15921
|
return [];
|
|
15762
|
-
const term = secondaryUniqueTerm(index, columns, columns.map((column) =>
|
|
15922
|
+
const term = secondaryUniqueTerm(index, columns, columns.map((column) => {
|
|
15923
|
+
const assigned = input.changes[column.name];
|
|
15924
|
+
return assigned === void 0 ? old[column.name] ?? null : assigned[row] ?? null;
|
|
15925
|
+
}));
|
|
15763
15926
|
return term === void 0 ? [] : [term];
|
|
15764
15927
|
});
|
|
15765
15928
|
assertNoDuplicateUniqueTerms(index, added);
|
|
@@ -18525,6 +18688,10 @@ function garbageCollectionProgress(job) {
|
|
|
18525
18688
|
function firesAfterTriggers(table, ...events) {
|
|
18526
18689
|
return (table.triggers ?? []).some((trigger) => trigger.timing === "after" && events.includes(trigger.event));
|
|
18527
18690
|
}
|
|
18691
|
+
function overlayOwnedSegments(segments, visibility) {
|
|
18692
|
+
const id = visibility?.overlayTransactionId;
|
|
18693
|
+
return id === void 0 ? [] : segments.filter((segment) => segment.transactionId === id);
|
|
18694
|
+
}
|
|
18528
18695
|
function collectRealTableNames(plan) {
|
|
18529
18696
|
const names = /* @__PURE__ */ new Set();
|
|
18530
18697
|
const excluded = /* @__PURE__ */ new Set();
|
|
@@ -14,6 +14,8 @@ async function createStore(descriptor, options) {
|
|
|
14
14
|
...descriptor.opfs?.durability === void 0 ? {} : { durability: descriptor.opfs.durability }
|
|
15
15
|
} : { kind, name: descriptor.name, ...descriptor.indexeddb }, options);
|
|
16
16
|
return opened;
|
|
17
|
+
}, {
|
|
18
|
+
opfsDatabaseExists: async (name) => (await import("../storage/opfs/index.js")).opfsDatabaseExists({ name })
|
|
17
19
|
});
|
|
18
20
|
}
|
|
19
21
|
if (descriptor.kind === "memory") {
|
|
@@ -538,7 +538,7 @@ class DatabaseRpcServer {
|
|
|
538
538
|
if (handle === void 0)
|
|
539
539
|
throw new Error(`Unknown handle: ${handleId}`);
|
|
540
540
|
if (handle.type === "write") {
|
|
541
|
-
this.#beginWriteHandleCall(handle);
|
|
541
|
+
await this.#beginWriteHandleCall(handle);
|
|
542
542
|
try {
|
|
543
543
|
return await this.#callWriteHandle(handleId, handle, method, args, context);
|
|
544
544
|
} finally {
|
|
@@ -847,12 +847,11 @@ class DatabaseRpcServer {
|
|
|
847
847
|
#releaseHandleId(id) {
|
|
848
848
|
this.#reservedHandleIds.delete(id);
|
|
849
849
|
}
|
|
850
|
-
#beginWriteHandleCall(handle) {
|
|
850
|
+
async #beginWriteHandleCall(handle) {
|
|
851
|
+
while (handle.activeCalls !== 0)
|
|
852
|
+
await handle.activeCallDone;
|
|
851
853
|
if (!handle.open)
|
|
852
854
|
throw new Error("Write handle is closed");
|
|
853
|
-
if (handle.activeCalls !== 0) {
|
|
854
|
-
throw new Error("Write handle already has a call in flight");
|
|
855
|
-
}
|
|
856
855
|
handle.activeCalls = 1;
|
|
857
856
|
handle.activeCallDone = new Promise((resolve) => {
|
|
858
857
|
handle.finishActiveCall = resolve;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IndexedDbBlockStore } from "../storage/indexeddb.js";
|
|
2
|
-
import { OpfsBlockStore } from "../storage/opfs/index.js";
|
|
2
|
+
import { OpfsBlockStore, opfsDatabaseExists } from "../storage/opfs/index.js";
|
|
3
3
|
import { openAutoStore } from "./auto-store.js";
|
|
4
4
|
import { unsupportedStoreKindError } from "./worker-server.js";
|
|
5
5
|
const autoWorkerStore = async (descriptor, options) => {
|
|
@@ -26,7 +26,7 @@ const autoWorkerStore = async (descriptor, options) => {
|
|
|
26
26
|
name: descriptor.name,
|
|
27
27
|
...descriptor.indexeddb?.durability === void 0 ? {} : { durability: descriptor.indexeddb.durability },
|
|
28
28
|
...descriptor.indexeddb?.uniqueKeyCacheBytes === void 0 ? {} : { uniqueKeyCacheBytes: descriptor.indexeddb.uniqueKeyCacheBytes }
|
|
29
|
-
}));
|
|
29
|
+
}), { opfsDatabaseExists: (name) => opfsDatabaseExists({ name }) });
|
|
30
30
|
default:
|
|
31
31
|
throw unsupportedStoreKindError("auto", descriptor.kind);
|
|
32
32
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const anonymous = /* @__PURE__ */ new WeakMap();
|
|
2
2
|
const named = /* @__PURE__ */ new Map();
|
|
3
|
+
const bypassing = /* @__PURE__ */ new Set();
|
|
4
|
+
const NOT_GRANTED = /* @__PURE__ */ Symbol("write admission lock not granted");
|
|
3
5
|
const WRITE_ADMISSION_WAIT_MS = 1e4;
|
|
6
|
+
function _resetWriteAdmissionForTests() {
|
|
7
|
+
bypassing.clear();
|
|
8
|
+
}
|
|
4
9
|
async function coordinateWrite(store, run, signal, options = {}) {
|
|
5
10
|
signal.throwIfAborted();
|
|
6
11
|
const name = store.liveQueryChannelName;
|
|
@@ -25,6 +30,19 @@ async function coordinateWrite(store, run, signal, options = {}) {
|
|
|
25
30
|
signal.throwIfAborted();
|
|
26
31
|
if (name === void 0 || locks === void 0)
|
|
27
32
|
return enter();
|
|
33
|
+
const lockName = `minnowdb-write:${name}`;
|
|
34
|
+
if (bypassing.has(name)) {
|
|
35
|
+
const result = await locks.request(lockName, { ifAvailable: true }, async (lock) => {
|
|
36
|
+
if (lock === null)
|
|
37
|
+
return NOT_GRANTED;
|
|
38
|
+
bypassing.delete(name);
|
|
39
|
+
return enter();
|
|
40
|
+
});
|
|
41
|
+
if (result !== NOT_GRANTED)
|
|
42
|
+
return result;
|
|
43
|
+
signal.throwIfAborted();
|
|
44
|
+
return await enter();
|
|
45
|
+
}
|
|
28
46
|
const startedAt = Date.now();
|
|
29
47
|
const wait = { ranOut: false };
|
|
30
48
|
const waitTimer = setTimeout(() => {
|
|
@@ -33,11 +51,12 @@ async function coordinateWrite(store, run, signal, options = {}) {
|
|
|
33
51
|
}, admissionWaitMs);
|
|
34
52
|
waitTimer.unref?.();
|
|
35
53
|
try {
|
|
36
|
-
return await locks.request(
|
|
54
|
+
return await locks.request(lockName, { signal: lockController.signal }, enter);
|
|
37
55
|
} catch (error) {
|
|
38
56
|
if (!wait.ranOut || admitted)
|
|
39
57
|
throw error;
|
|
40
58
|
signal.throwIfAborted();
|
|
59
|
+
bypassing.add(name);
|
|
41
60
|
options.onAdmissionWaitExceeded?.(Date.now() - startedAt);
|
|
42
61
|
return await enter();
|
|
43
62
|
} finally {
|
|
@@ -72,5 +91,6 @@ async function coordinateWrite(store, run, signal, options = {}) {
|
|
|
72
91
|
}
|
|
73
92
|
export {
|
|
74
93
|
WRITE_ADMISSION_WAIT_MS,
|
|
94
|
+
_resetWriteAdmissionForTests,
|
|
75
95
|
coordinateWrite
|
|
76
96
|
};
|