@minnowdb/core 0.2.1 → 0.3.0
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/README.md +21 -32
- package/dist/engine/artifact-cache.d.ts +2 -0
- package/dist/engine/artifact-cache.d.ts.map +1 -1
- package/dist/engine/artifact-cache.js +5 -0
- package/dist/engine/artifact-cache.js.map +1 -1
- package/dist/engine/batch.d.ts +6 -1
- package/dist/engine/batch.d.ts.map +1 -1
- package/dist/engine/batch.js +17 -6
- package/dist/engine/batch.js.map +1 -1
- package/dist/engine/catalog.d.ts +22 -2
- package/dist/engine/catalog.d.ts.map +1 -1
- package/dist/engine/catalog.js +30 -3
- package/dist/engine/catalog.js.map +1 -1
- package/dist/engine/client.d.ts +2 -0
- package/dist/engine/client.d.ts.map +1 -1
- package/dist/engine/client.js +20 -10
- package/dist/engine/client.js.map +1 -1
- package/dist/engine/database.d.ts +67 -5
- package/dist/engine/database.d.ts.map +1 -1
- package/dist/engine/database.js +3508 -369
- package/dist/engine/database.js.map +1 -1
- package/dist/engine/defaults.d.ts +4 -7
- package/dist/engine/defaults.d.ts.map +1 -1
- package/dist/engine/defaults.js +47 -21
- package/dist/engine/defaults.js.map +1 -1
- package/dist/engine/fts.d.ts.map +1 -1
- package/dist/engine/fts.js +2 -0
- package/dist/engine/fts.js.map +1 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/live.d.ts.map +1 -1
- package/dist/engine/live.js +10 -2
- package/dist/engine/live.js.map +1 -1
- package/dist/engine/optimizer.d.ts.map +1 -1
- package/dist/engine/optimizer.js +258 -23
- package/dist/engine/optimizer.js.map +1 -1
- package/dist/engine/query-cache.d.ts +1 -1
- package/dist/engine/query-cache.d.ts.map +1 -1
- package/dist/engine/query-cache.js +3 -1
- package/dist/engine/query-cache.js.map +1 -1
- package/dist/engine/query.d.ts +130 -22
- package/dist/engine/query.d.ts.map +1 -1
- package/dist/engine/query.js +1344 -241
- package/dist/engine/query.js.map +1 -1
- package/dist/engine/schema-wire.d.ts +6 -2
- package/dist/engine/schema-wire.d.ts.map +1 -1
- package/dist/engine/schema-wire.js +40 -33
- package/dist/engine/schema-wire.js.map +1 -1
- package/dist/engine/schema.d.ts +157 -76
- package/dist/engine/schema.d.ts.map +1 -1
- package/dist/engine/schema.js +548 -103
- package/dist/engine/schema.js.map +1 -1
- package/dist/engine/sort-keys.d.ts +4 -3
- package/dist/engine/sort-keys.d.ts.map +1 -1
- package/dist/engine/sort-keys.js +42 -27
- package/dist/engine/sort-keys.js.map +1 -1
- package/dist/engine/sql-domains.d.ts +26 -0
- package/dist/engine/sql-domains.d.ts.map +1 -0
- package/dist/engine/sql-domains.js +421 -0
- package/dist/engine/sql-domains.js.map +1 -0
- package/dist/engine/sql-driver.d.ts +19 -0
- package/dist/engine/sql-driver.d.ts.map +1 -0
- package/dist/engine/sql-driver.js +2 -0
- package/dist/engine/sql-driver.js.map +1 -0
- package/dist/engine/sql-json.d.ts +7 -8
- package/dist/engine/sql-json.d.ts.map +1 -1
- package/dist/engine/sql-json.js +28 -14
- package/dist/engine/sql-json.js.map +1 -1
- package/dist/engine/sql-semantics.d.ts +2 -0
- package/dist/engine/sql-semantics.d.ts.map +1 -1
- package/dist/engine/sql-semantics.js +69 -3
- package/dist/engine/sql-semantics.js.map +1 -1
- package/dist/engine/vector.d.ts +5 -1
- package/dist/engine/vector.d.ts.map +1 -1
- package/dist/engine/vector.js +433 -61
- package/dist/engine/vector.js.map +1 -1
- package/dist/engine/worker-host.d.ts +1 -0
- package/dist/engine/worker-host.d.ts.map +1 -1
- package/dist/engine/worker-host.js +4 -0
- package/dist/engine/worker-host.js.map +1 -1
- package/dist/plan/index.d.ts +3 -3
- package/dist/plan/index.js +3 -3
- package/dist/storage/indexeddb.d.ts +44 -5
- package/dist/storage/indexeddb.d.ts.map +1 -1
- package/dist/storage/indexeddb.js +738 -175
- package/dist/storage/indexeddb.js.map +1 -1
- package/dist/storage/memory.d.ts +27 -10
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +67 -18
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/opfs/leader.d.ts +28 -6
- package/dist/storage/opfs/leader.d.ts.map +1 -1
- package/dist/storage/opfs/leader.js +301 -34
- package/dist/storage/opfs/leader.js.map +1 -1
- package/dist/storage/opfs/rpc.d.ts.map +1 -1
- package/dist/storage/opfs/rpc.js +2 -1
- package/dist/storage/opfs/rpc.js.map +1 -1
- package/dist/storage/opfs/store.d.ts +34 -5
- package/dist/storage/opfs/store.d.ts.map +1 -1
- package/dist/storage/opfs/store.js +41 -0
- package/dist/storage/opfs/store.js.map +1 -1
- package/dist/storage/snapshot.d.ts +14 -0
- package/dist/storage/snapshot.d.ts.map +1 -1
- package/dist/storage/snapshot.js +44 -0
- package/dist/storage/snapshot.js.map +1 -1
- package/dist/storage/toolkit/index.d.ts +1 -1
- package/dist/storage/toolkit/index.d.ts.map +1 -1
- package/dist/storage/toolkit/index.js +1 -1
- package/dist/storage/toolkit/index.js.map +1 -1
- package/dist/storage/toolkit/record-core.d.ts +19 -5
- package/dist/storage/toolkit/record-core.d.ts.map +1 -1
- package/dist/storage/toolkit/record-core.js +353 -78
- package/dist/storage/toolkit/record-core.js.map +1 -1
- package/dist/storage/toolkit/wire.d.ts +15 -0
- package/dist/storage/toolkit/wire.d.ts.map +1 -1
- package/dist/storage/toolkit/wire.js +134 -0
- package/dist/storage/toolkit/wire.js.map +1 -1
- package/dist/storage/types.d.ts +213 -31
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +291 -20
- package/dist/storage/types.js.map +1 -1
- package/dist/testing/block-store-conformance.d.ts.map +1 -1
- package/dist/testing/block-store-conformance.js +153 -2
- package/dist/testing/block-store-conformance.js.map +1 -1
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +23 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/simulator.d.ts +98 -0
- package/dist/testing/simulator.d.ts.map +1 -0
- package/dist/testing/simulator.js +560 -0
- package/dist/testing/simulator.js.map +1 -0
- package/dist/testing/sqllogictest.d.ts +90 -0
- package/dist/testing/sqllogictest.d.ts.map +1 -0
- package/dist/testing/sqllogictest.js +435 -0
- package/dist/testing/sqllogictest.js.map +1 -0
- package/dist/transactions/index.d.ts +18 -3
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/index.js +64 -8
- package/dist/transactions/index.js.map +1 -1
- package/package.json +5 -3
- package/postgres-feature-profile.json +223 -0
- package/sql-feature-matrix.json +172 -252
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CompactionJobConflictError, createManifest, createGarbageCollectionJobRecord, advanceGarbageCollectionJobRecord, collectFtsCandidates, invalidateUncoveredFtsColumns, GarbageCollectionJobConflictError, LeaseConflictError, SnapshotManifestMissingError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, normalizeCompactionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord, updateTransactionRecord, WriteConflictError, } from "../types.js";
|
|
2
|
-
import { selectLiveRecords } from "../snapshot.js";
|
|
1
|
+
import { CompactionJobConflictError, createManifest, createGarbageCollectionJobRecord, advanceGarbageCollectionJobRecord, collectFtsCandidates, collectFtsPostings, activePostingStorageColumnIds, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, SnapshotManifestMissingError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, normalizeCompactionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord, updateTransactionRecord, validateTableColumns, validateSecondaryIndexes, secondaryIndexColumnIds, secondaryUniqueKeyNamespace, WriteConflictError, } from "../types.js";
|
|
2
|
+
import { selectLiveRecords, validateSnapshotCatalog, } from "../snapshot.js";
|
|
3
3
|
/**
|
|
4
4
|
* The synchronous record-state machine behind `MemoryBlockStore` and `OpfsBlockStore`, offered
|
|
5
5
|
* to adapter authors as the semantics half of a `BlockStore`: every record family the contract
|
|
@@ -92,23 +92,47 @@ export class RecordCore {
|
|
|
92
92
|
*/
|
|
93
93
|
listTempOwnerIdsPage(afterOwnerId, limit, extraOwnerIds) {
|
|
94
94
|
validatePageLimit(limit);
|
|
95
|
-
const
|
|
95
|
+
const records = [];
|
|
96
|
+
const add = (ownerId) => {
|
|
97
|
+
if (afterOwnerId !== null && ownerId <= afterOwnerId)
|
|
98
|
+
return;
|
|
99
|
+
const index = records.findIndex((existing) => existing >= ownerId);
|
|
100
|
+
if (records[index] === ownerId)
|
|
101
|
+
return;
|
|
102
|
+
if (index < 0)
|
|
103
|
+
records.push(ownerId);
|
|
104
|
+
else
|
|
105
|
+
records.splice(index, 0, ownerId);
|
|
106
|
+
if (records.length > limit)
|
|
107
|
+
records.pop();
|
|
108
|
+
};
|
|
109
|
+
for (const ownerId of this.#tempOwners.keys())
|
|
110
|
+
add(ownerId);
|
|
96
111
|
for (const ownerId of extraOwnerIds)
|
|
97
|
-
|
|
98
|
-
const sorted = [...ownerIds]
|
|
99
|
-
.filter((ownerId) => afterOwnerId === null || ownerId > afterOwnerId)
|
|
100
|
-
.sort();
|
|
101
|
-
const records = sorted.slice(0, limit);
|
|
112
|
+
add(ownerId);
|
|
102
113
|
return {
|
|
103
114
|
records,
|
|
104
|
-
nextCursor:
|
|
115
|
+
nextCursor: records.length === limit ? (records.at(-1) ?? null) : null,
|
|
105
116
|
};
|
|
106
117
|
}
|
|
107
118
|
addTable(record) {
|
|
119
|
+
validateTableColumns(record.columns);
|
|
120
|
+
validateSecondaryIndexes(record);
|
|
108
121
|
if (this.#tables.has(record.id))
|
|
109
122
|
throw new Error(`Table already exists: ${record.id}`);
|
|
110
123
|
if (this.#tableIdsByName.has(record.name))
|
|
111
124
|
throw new Error(`Table name already exists: ${record.name}`);
|
|
125
|
+
const newIndexNames = new Set();
|
|
126
|
+
for (const index of Object.values(record.secondaryIndexes ?? {})) {
|
|
127
|
+
if (newIndexNames.has(index.name))
|
|
128
|
+
throw new TypeError(`Index already exists: ${index.name}`);
|
|
129
|
+
newIndexNames.add(index.name);
|
|
130
|
+
for (const table of this.#tables.values()) {
|
|
131
|
+
if (Object.values(table.secondaryIndexes ?? {}).some((existing) => existing.name === index.name)) {
|
|
132
|
+
throw new TypeError(`Index already exists: ${index.name}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
112
136
|
this.#tables.set(record.id, structuredClone(record));
|
|
113
137
|
this.#tableIdsByName.set(record.name, record.id);
|
|
114
138
|
this.#catalogEpoch += 1;
|
|
@@ -123,10 +147,50 @@ export class RecordCore {
|
|
|
123
147
|
if (record === undefined || actualRevision !== expectedRevision) {
|
|
124
148
|
throw new TableRecordConflictError(id, expectedRevision, actualRevision);
|
|
125
149
|
}
|
|
150
|
+
if (update.expectedManifestVersion !== undefined &&
|
|
151
|
+
(this.getCurrentManifestVersion() ?? null) !== update.expectedManifestVersion.value) {
|
|
152
|
+
throw new WriteConflictError(update.expectedManifestVersion.value, this.getCurrentManifestVersion());
|
|
153
|
+
}
|
|
154
|
+
let uniqueSeed;
|
|
155
|
+
if (update.uniqueKeySeed !== undefined) {
|
|
156
|
+
uniqueSeed = new Set();
|
|
157
|
+
for (const token of update.uniqueKeySeed.keyTokens) {
|
|
158
|
+
if (uniqueSeed.has(token))
|
|
159
|
+
throw new UniqueKeyConflictError(update.uniqueKeySeed.namespaceId, token);
|
|
160
|
+
uniqueSeed.add(token);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
126
163
|
if (update.columns !== undefined)
|
|
127
164
|
validateTableColumns(update.columns);
|
|
128
|
-
const { ftsColumns: previousFts, triggers: previousTriggers, ...base } = record;
|
|
129
|
-
|
|
165
|
+
const { ftsColumns: previousFts, secondaryIndexes: previousSecondary, triggers: previousTriggers, ...base } = record;
|
|
166
|
+
let nextFts = update.ftsColumns === undefined ? previousFts : update.ftsColumns;
|
|
167
|
+
let nextSecondary = update.secondaryIndexes === undefined ? previousSecondary : update.secondaryIndexes;
|
|
168
|
+
const retainedColumnIds = update.columns === undefined
|
|
169
|
+
? undefined
|
|
170
|
+
: new Set(update.columns.map(({ id: columnId }) => columnId));
|
|
171
|
+
if (nextFts !== null && nextFts !== undefined && retainedColumnIds !== undefined) {
|
|
172
|
+
nextFts = Object.fromEntries(Object.entries(nextFts).filter(([columnId]) => retainedColumnIds.has(columnId)));
|
|
173
|
+
if (Object.keys(nextFts).length === 0)
|
|
174
|
+
nextFts = null;
|
|
175
|
+
}
|
|
176
|
+
if (nextSecondary !== null && nextSecondary !== undefined && retainedColumnIds !== undefined) {
|
|
177
|
+
nextSecondary = Object.fromEntries(Object.entries(nextSecondary).filter(([, index]) => secondaryIndexColumnIds(index).every((columnId) => retainedColumnIds.has(columnId))));
|
|
178
|
+
if (Object.keys(nextSecondary).length === 0)
|
|
179
|
+
nextSecondary = null;
|
|
180
|
+
}
|
|
181
|
+
const nextIndexNames = new Set();
|
|
182
|
+
for (const index of Object.values(nextSecondary ?? {})) {
|
|
183
|
+
if (nextIndexNames.has(index.name))
|
|
184
|
+
throw new TypeError(`Index already exists: ${index.name}`);
|
|
185
|
+
nextIndexNames.add(index.name);
|
|
186
|
+
for (const [otherTableId, table] of this.#tables) {
|
|
187
|
+
if (otherTableId === id)
|
|
188
|
+
continue;
|
|
189
|
+
if (Object.values(table.secondaryIndexes ?? {}).some((existing) => existing.name === index.name)) {
|
|
190
|
+
throw new TypeError(`Index already exists: ${index.name}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
130
194
|
const nextTriggers = update.triggers === undefined ? previousTriggers : update.triggers;
|
|
131
195
|
const updated = {
|
|
132
196
|
...base,
|
|
@@ -134,11 +198,44 @@ export class RecordCore {
|
|
|
134
198
|
...(nextFts === null || nextFts === undefined
|
|
135
199
|
? {}
|
|
136
200
|
: { ftsColumns: structuredClone(nextFts) }),
|
|
201
|
+
...(nextSecondary === null || nextSecondary === undefined
|
|
202
|
+
? {}
|
|
203
|
+
: { secondaryIndexes: structuredClone(nextSecondary) }),
|
|
137
204
|
...(nextTriggers === null || nextTriggers === undefined
|
|
138
205
|
? {}
|
|
139
206
|
: { triggers: structuredClone(nextTriggers) }),
|
|
140
207
|
revision: expectedRevision + 1,
|
|
141
208
|
};
|
|
209
|
+
validateSecondaryIndexes(updated);
|
|
210
|
+
if (update.uniqueKeySeed !== undefined) {
|
|
211
|
+
const ownsSeed = Object.entries(updated.secondaryIndexes ?? {}).some(([indexId, index]) => index.unique === true &&
|
|
212
|
+
index.uniqueEnforced === true &&
|
|
213
|
+
index.state === "ready" &&
|
|
214
|
+
secondaryUniqueKeyNamespace(updated.id, indexId) === update.uniqueKeySeed?.namespaceId);
|
|
215
|
+
if (!ownsSeed) {
|
|
216
|
+
throw new TypeError("UNIQUE-index seed does not belong to a ready catalog index");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (retainedColumnIds !== undefined) {
|
|
220
|
+
for (const column of record.columns) {
|
|
221
|
+
if (!retainedColumnIds.has(column.id))
|
|
222
|
+
this.removeFtsColumn(record.id, column.id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const retainedIndexStorage = new Set(Object.values(nextSecondary ?? {}).map((index) => index.storageColumnId));
|
|
226
|
+
for (const index of Object.values(previousSecondary ?? {})) {
|
|
227
|
+
if (!retainedIndexStorage.has(index.storageColumnId)) {
|
|
228
|
+
this.removeFtsColumn(record.id, index.storageColumnId);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (const [indexId, previous] of Object.entries(previousSecondary ?? {})) {
|
|
232
|
+
if (previous.unique === true && nextSecondary?.[indexId]?.unique !== true) {
|
|
233
|
+
this.#uniqueKeys.delete(secondaryUniqueKeyNamespace(record.id, indexId));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (update.uniqueKeySeed !== undefined) {
|
|
237
|
+
this.#uniqueKeys.set(update.uniqueKeySeed.namespaceId, uniqueSeed ?? new Set());
|
|
238
|
+
}
|
|
142
239
|
this.#tables.set(id, updated);
|
|
143
240
|
this.#catalogEpoch += 1;
|
|
144
241
|
return structuredClone(updated);
|
|
@@ -167,6 +264,11 @@ export class RecordCore {
|
|
|
167
264
|
this.#nextAutoIncrement.delete(key);
|
|
168
265
|
}
|
|
169
266
|
this.#uniqueKeys.delete(id);
|
|
267
|
+
const secondaryUniquePrefix = `${id}\u0000secondary-index\u0000`;
|
|
268
|
+
for (const namespaceId of [...this.#uniqueKeys.keys()]) {
|
|
269
|
+
if (namespaceId.startsWith(secondaryUniquePrefix))
|
|
270
|
+
this.#uniqueKeys.delete(namespaceId);
|
|
271
|
+
}
|
|
170
272
|
this.#nextRowIds.delete(id);
|
|
171
273
|
this.#tableIdsByName.delete(record.name);
|
|
172
274
|
this.#tables.delete(id);
|
|
@@ -183,6 +285,11 @@ export class RecordCore {
|
|
|
183
285
|
}
|
|
184
286
|
}
|
|
185
287
|
}
|
|
288
|
+
removeFtsColumn(tableId, columnId) {
|
|
289
|
+
const key = `${tableId}/${columnId}`;
|
|
290
|
+
this.#ftsBases.delete(key);
|
|
291
|
+
this.#ftsDeltas.delete(key);
|
|
292
|
+
}
|
|
186
293
|
readFtsCandidates(tableId, columnId, terms, upToVersion) {
|
|
187
294
|
const key = `${tableId}/${columnId}`;
|
|
188
295
|
const base = this.#ftsBases.get(key);
|
|
@@ -203,6 +310,7 @@ export class RecordCore {
|
|
|
203
310
|
deltaChunkCount,
|
|
204
311
|
totalTokens,
|
|
205
312
|
coversVersion: base?.coversVersion ?? -1,
|
|
313
|
+
hasBase: base !== undefined,
|
|
206
314
|
};
|
|
207
315
|
}
|
|
208
316
|
/**
|
|
@@ -210,8 +318,9 @@ export class RecordCore {
|
|
|
210
318
|
* segments to a table with an active index but no delta for one of its columns flips that
|
|
211
319
|
* column to "invalid" so a rebuild self-heals instead of the data commit failing.
|
|
212
320
|
*/
|
|
213
|
-
#applyFtsChanges(pendingSegments, changeList, version) {
|
|
321
|
+
#applyFtsChanges(pendingSegments, changeList, version, logicallyChangedTableIds) {
|
|
214
322
|
const changedTableIds = new Set(pendingSegments.map((segment) => segment.tableId));
|
|
323
|
+
const scalarChangedTableIds = logicallyChangedTableIds === undefined ? changedTableIds : new Set(logicallyChangedTableIds);
|
|
215
324
|
for (const tableId of changedTableIds) {
|
|
216
325
|
const record = this.#tables.get(tableId);
|
|
217
326
|
if (record === undefined)
|
|
@@ -219,14 +328,26 @@ export class RecordCore {
|
|
|
219
328
|
const forTable = (changeList ?? []).find((entry) => entry.tableId === tableId);
|
|
220
329
|
const covered = new Set(forTable?.columns.map((column) => column.columnId) ?? []);
|
|
221
330
|
const invalidated = invalidateUncoveredFtsColumns(record, covered);
|
|
222
|
-
|
|
331
|
+
const withFts = invalidated ?? record;
|
|
332
|
+
const withSecondary = scalarChangedTableIds.has(tableId)
|
|
333
|
+
? invalidateUncoveredSecondaryIndexes(withFts, covered)
|
|
334
|
+
: undefined;
|
|
335
|
+
if (withSecondary !== undefined)
|
|
336
|
+
this.#tables.set(record.id, withSecondary);
|
|
337
|
+
else if (invalidated !== undefined)
|
|
223
338
|
this.#tables.set(record.id, invalidated);
|
|
224
339
|
}
|
|
225
340
|
for (const changes of changeList ?? [])
|
|
226
341
|
this.#applyFtsEntry(changes, version);
|
|
227
342
|
}
|
|
228
343
|
#applyFtsEntry(changes, version) {
|
|
344
|
+
const table = this.#tables.get(changes.tableId);
|
|
345
|
+
if (table === undefined)
|
|
346
|
+
return;
|
|
347
|
+
const active = activePostingStorageColumnIds(table);
|
|
229
348
|
for (const column of changes.columns) {
|
|
349
|
+
if (!active.has(column.columnId))
|
|
350
|
+
continue;
|
|
230
351
|
const key = `${changes.tableId}/${column.columnId}`;
|
|
231
352
|
const deltas = this.#ftsDeltas.get(key) ??
|
|
232
353
|
new Map();
|
|
@@ -256,6 +377,17 @@ export class RecordCore {
|
|
|
256
377
|
}
|
|
257
378
|
return { chunkLists, deltaChunkCount, deltaTokens };
|
|
258
379
|
}
|
|
380
|
+
readFtsPostings(tableId, columnId, upToVersion) {
|
|
381
|
+
const base = this.#ftsBases.get(`${tableId}/${columnId}`);
|
|
382
|
+
const coversVersion = base?.coversVersion ?? -1;
|
|
383
|
+
const delta = this.readFtsDeltas(tableId, columnId, coversVersion, upToVersion);
|
|
384
|
+
return {
|
|
385
|
+
postings: collectFtsPostings([...(base?.chunks ?? []), ...delta.chunkLists]),
|
|
386
|
+
deltaChunkCount: delta.deltaChunkCount,
|
|
387
|
+
coversVersion,
|
|
388
|
+
hasBase: base !== undefined,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
259
391
|
/** The delta-pruning half of `writeFtsBase`, for stores that keep the base itself outside. */
|
|
260
392
|
pruneFtsDeltas(tableId, columnId, coversVersion) {
|
|
261
393
|
const deltas = this.#ftsDeltas.get(`${tableId}/${columnId}`);
|
|
@@ -316,6 +448,11 @@ export class RecordCore {
|
|
|
316
448
|
.sort((left, right) => left.id.localeCompare(right.id))
|
|
317
449
|
.map((record) => normalizeSegmentRecord(record));
|
|
318
450
|
}
|
|
451
|
+
listSegmentPage(afterId, limit) {
|
|
452
|
+
validatePageLimit(limit);
|
|
453
|
+
const records = boundedRecordPage(this.#segments.values(), afterId, limit, (record) => record.id).map((record) => normalizeSegmentRecord(record));
|
|
454
|
+
return { records, nextCursor: records.length === limit ? (records.at(-1)?.id ?? null) : null };
|
|
455
|
+
}
|
|
319
456
|
removeSegment(id) {
|
|
320
457
|
this.#segments.delete(id);
|
|
321
458
|
}
|
|
@@ -354,16 +491,24 @@ export class RecordCore {
|
|
|
354
491
|
getQueryCatalogState(tableNames) {
|
|
355
492
|
const tables = tableNames.map((name) => this.getTableByName(name));
|
|
356
493
|
const foundTableIds = new Set(tables.filter((table) => table !== undefined).map((table) => table.id));
|
|
357
|
-
|
|
358
|
-
|
|
494
|
+
const currentManifest = this.#currentVersion === null ? undefined : this.#manifests.get(this.#currentVersion);
|
|
495
|
+
const visibleBlockIds = new Set(currentManifest?.blockIds ?? []);
|
|
496
|
+
// Filter raw records first: normalizing and sorting every segment in the database to keep a
|
|
497
|
+
// handful is the wrong order of operations on a hot path. This read is anchored to the
|
|
498
|
+
// current manifest version it returns, so segments already superseded from that manifest are
|
|
499
|
+
// historical records, not query inputs. Explicit version reads use listSegments instead.
|
|
359
500
|
const segments = [...this.#segments.values()]
|
|
360
|
-
.filter((record) => foundTableIds.has(record.tableId)
|
|
501
|
+
.filter((record) => foundTableIds.has(record.tableId) &&
|
|
502
|
+
Object.values(record.columnBlockIds)
|
|
503
|
+
.flat()
|
|
504
|
+
.every((blockId) => visibleBlockIds.has(blockId)))
|
|
361
505
|
.sort((left, right) => left.id.localeCompare(right.id))
|
|
362
506
|
.map((record) => normalizeSegmentRecord(record));
|
|
363
507
|
const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
|
|
364
508
|
const transactions = this.getTransactions(transactionIds).filter((record) => record !== undefined);
|
|
365
509
|
return {
|
|
366
510
|
manifestVersion: this.#currentVersion,
|
|
511
|
+
manifestBlockIds: [...visibleBlockIds].sort(),
|
|
367
512
|
tables,
|
|
368
513
|
segments,
|
|
369
514
|
transactions,
|
|
@@ -385,11 +530,7 @@ export class RecordCore {
|
|
|
385
530
|
}
|
|
386
531
|
listManifestPage(afterVersion, limit) {
|
|
387
532
|
validatePageLimit(limit);
|
|
388
|
-
const records =
|
|
389
|
-
.filter((manifest) => afterVersion === null || manifest.version > afterVersion)
|
|
390
|
-
.sort((left, right) => left.version - right.version)
|
|
391
|
-
.slice(0, limit)
|
|
392
|
-
.map((manifest) => structuredClone(manifest));
|
|
533
|
+
const records = boundedRecordPage(this.#manifests.values(), afterVersion, limit, (manifest) => manifest.version).map((manifest) => structuredClone(manifest));
|
|
393
534
|
return {
|
|
394
535
|
records,
|
|
395
536
|
nextCursor: records.length === limit ? (records.at(-1)?.version ?? null) : null,
|
|
@@ -487,11 +628,7 @@ export class RecordCore {
|
|
|
487
628
|
}
|
|
488
629
|
listTransactionPage(afterId, limit) {
|
|
489
630
|
validatePageLimit(limit);
|
|
490
|
-
const records =
|
|
491
|
-
.filter((record) => afterId === null || record.id > afterId)
|
|
492
|
-
.sort((left, right) => left.id.localeCompare(right.id))
|
|
493
|
-
.slice(0, limit)
|
|
494
|
-
.map((record) => structuredClone(record));
|
|
631
|
+
const records = boundedRecordPage(this.#transactions.values(), afterId, limit, (record) => record.id).map((record) => structuredClone(record));
|
|
495
632
|
return { records, nextCursor: records.length === limit ? (records.at(-1)?.id ?? null) : null };
|
|
496
633
|
}
|
|
497
634
|
updateTransaction(id, expectedRevision, update) {
|
|
@@ -672,6 +809,22 @@ export class RecordCore {
|
|
|
672
809
|
return segment;
|
|
673
810
|
});
|
|
674
811
|
const uniqueKeyEntries = input.uniqueKeyChanges ?? [];
|
|
812
|
+
const coveredUniqueNamespaces = new Set(uniqueKeyEntries.map((entry) => entry.tableId));
|
|
813
|
+
const changedTableIds = new Set([
|
|
814
|
+
...(input.changedTableIds ?? []),
|
|
815
|
+
...pendingSegments.map((segment) => segment.tableId),
|
|
816
|
+
]);
|
|
817
|
+
for (const tableId of changedTableIds) {
|
|
818
|
+
const changedTable = this.#tables.get(tableId);
|
|
819
|
+
if (changedTable === undefined)
|
|
820
|
+
continue;
|
|
821
|
+
for (const [indexId, index] of Object.entries(changedTable.secondaryIndexes ?? {})) {
|
|
822
|
+
if (index.uniqueEnforced === true &&
|
|
823
|
+
!coveredUniqueNamespaces.has(secondaryUniqueKeyNamespace(tableId, indexId))) {
|
|
824
|
+
throw new UniqueIndexCoverageError(tableId, index.name);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
675
828
|
// Entries apply in operation order over per-table deltas against the live membership, so
|
|
676
829
|
// a scope that inserts the same key twice conflicts exactly like two separate commits
|
|
677
830
|
// would. A delta rather than a working copy of the table's whole key set: the copy made
|
|
@@ -741,12 +894,20 @@ export class RecordCore {
|
|
|
741
894
|
for (const token of delta.added)
|
|
742
895
|
tokens.add(token);
|
|
743
896
|
}
|
|
744
|
-
this.#applyFtsChanges(pendingSegments, plan.ftsChanges, manifest.version);
|
|
897
|
+
this.#applyFtsChanges(pendingSegments, plan.ftsChanges, manifest.version, manifest.changedTableIds);
|
|
745
898
|
this.#catalogEpoch += 1;
|
|
746
899
|
// Match the IndexedDB store's observable commit shape: the summary without blockIds.
|
|
747
900
|
const { blockIds: _resolved, ...summary } = manifest;
|
|
748
901
|
void _resolved;
|
|
749
|
-
|
|
902
|
+
const ftsDeltaCounts = (plan.ftsChanges ?? []).flatMap((changes) => changes.columns.map((column) => ({
|
|
903
|
+
tableId: changes.tableId,
|
|
904
|
+
columnId: column.columnId,
|
|
905
|
+
count: this.#ftsDeltas.get(`${changes.tableId}/${column.columnId}`)?.size ?? 0,
|
|
906
|
+
})));
|
|
907
|
+
return structuredClone({
|
|
908
|
+
...summary,
|
|
909
|
+
...(ftsDeltaCounts.length === 0 ? {} : { ftsDeltaCounts }),
|
|
910
|
+
});
|
|
750
911
|
}
|
|
751
912
|
createLease(record) {
|
|
752
913
|
validateLeaseExpiration(record.expiresAt);
|
|
@@ -822,11 +983,7 @@ export class RecordCore {
|
|
|
822
983
|
}
|
|
823
984
|
listCompactionJobPage(afterId, limit) {
|
|
824
985
|
validatePageLimit(limit);
|
|
825
|
-
const records =
|
|
826
|
-
.filter((record) => afterId === null || record.id > afterId)
|
|
827
|
-
.sort((left, right) => left.id.localeCompare(right.id))
|
|
828
|
-
.slice(0, limit)
|
|
829
|
-
.map((record) => structuredClone(record));
|
|
986
|
+
const records = boundedRecordPage(this.#compactionJobs.values(), afterId, limit, (record) => record.id).map((record) => structuredClone(record));
|
|
830
987
|
return { records, nextCursor: records.length === limit ? (records.at(-1)?.id ?? null) : null };
|
|
831
988
|
}
|
|
832
989
|
updateCompactionJob(id, expectedRevision, update) {
|
|
@@ -960,7 +1117,7 @@ export class RecordCore {
|
|
|
960
1117
|
segmentIndex + segmentIdsToExamine.length === current.candidateSegmentIds.length
|
|
961
1118
|
? current.candidateBlockIds.slice(current.cursor.blockIndex, current.cursor.blockIndex + blockCapacity)
|
|
962
1119
|
: [];
|
|
963
|
-
const roots = collectBoundedPhysicalRoots(segmentIdsToExamine, blockIdsToExamine, this.#manifests
|
|
1120
|
+
const roots = collectBoundedPhysicalRoots(segmentIdsToExamine, blockIdsToExamine, this.#manifests, prunedManifestVersionSet, this.#segments, this.#transactions, this.#compactionJobs);
|
|
964
1121
|
while (remaining > 0 &&
|
|
965
1122
|
manifestIndex === current.candidateManifestVersions.length &&
|
|
966
1123
|
segmentIndex < current.candidateSegmentIds.length) {
|
|
@@ -1156,10 +1313,23 @@ export class RecordCore {
|
|
|
1156
1313
|
ftsColumns[columnId] = { ...state, state: "invalid" };
|
|
1157
1314
|
}
|
|
1158
1315
|
}
|
|
1316
|
+
const secondaryIndexes = { ...(table.secondaryIndexes ?? {}) };
|
|
1317
|
+
for (const [indexId, state] of Object.entries(secondaryIndexes)) {
|
|
1318
|
+
const base = this.#ftsBases.get(`${table.id}/${state.storageColumnId}`);
|
|
1319
|
+
if (state.state === "ready" && base?.coversVersion === version) {
|
|
1320
|
+
fts.push({ columnId: state.storageColumnId, ...structuredClone(base) });
|
|
1321
|
+
}
|
|
1322
|
+
else {
|
|
1323
|
+
const { buildId: _abandonedBuild, ...invalid } = state;
|
|
1324
|
+
void _abandonedBuild;
|
|
1325
|
+
secondaryIndexes[indexId] = { ...invalid, state: "invalid" };
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1159
1328
|
return {
|
|
1160
1329
|
record: {
|
|
1161
1330
|
...structuredClone(table),
|
|
1162
1331
|
...(Object.keys(ftsColumns).length === 0 ? {} : { ftsColumns }),
|
|
1332
|
+
...(Object.keys(secondaryIndexes).length === 0 ? {} : { secondaryIndexes }),
|
|
1163
1333
|
},
|
|
1164
1334
|
nextRowId: this.#nextRowIds.get(table.id) ?? 1n,
|
|
1165
1335
|
autoIncrement: table.columns.flatMap((column) => {
|
|
@@ -1167,6 +1337,17 @@ export class RecordCore {
|
|
|
1167
1337
|
return next === undefined ? [] : [{ columnId: column.id, next }];
|
|
1168
1338
|
}),
|
|
1169
1339
|
uniqueKeyTokens: [...(this.#uniqueKeys.get(table.id) ?? [])],
|
|
1340
|
+
secondaryUniqueKeys: Object.entries(table.secondaryIndexes ?? {}).flatMap(([indexId, index]) => index.uniqueEnforced === true
|
|
1341
|
+
? [
|
|
1342
|
+
{
|
|
1343
|
+
indexId,
|
|
1344
|
+
keyTokens: [
|
|
1345
|
+
...(this.#uniqueKeys.get(secondaryUniqueKeyNamespace(table.id, indexId)) ??
|
|
1346
|
+
[]),
|
|
1347
|
+
],
|
|
1348
|
+
},
|
|
1349
|
+
]
|
|
1350
|
+
: []),
|
|
1170
1351
|
fts,
|
|
1171
1352
|
};
|
|
1172
1353
|
});
|
|
@@ -1190,12 +1371,22 @@ export class RecordCore {
|
|
|
1190
1371
|
this.loadSnapshot(snapshot, storeBlockBytes);
|
|
1191
1372
|
}
|
|
1192
1373
|
loadSnapshot(snapshot, storeBlockBytes) {
|
|
1374
|
+
validateSnapshotCatalog(snapshot.tables);
|
|
1193
1375
|
for (const block of snapshot.blocks)
|
|
1194
1376
|
storeBlockBytes(block.id, block.bytes);
|
|
1195
1377
|
this.loadSnapshotRecords({ ...snapshot, blocks: undefined }, snapshot.blocks.map((block) => block.id));
|
|
1196
1378
|
}
|
|
1197
1379
|
/** The record half of a snapshot load; block bytes are the caller's, ids theirs to name. */
|
|
1198
1380
|
loadSnapshotRecords(snapshot, blockIds) {
|
|
1381
|
+
validateSnapshotCatalog(snapshot.tables);
|
|
1382
|
+
const indexNames = new Set();
|
|
1383
|
+
for (const table of snapshot.tables) {
|
|
1384
|
+
for (const index of Object.values(table.record.secondaryIndexes ?? {})) {
|
|
1385
|
+
if (indexNames.has(index.name))
|
|
1386
|
+
throw new TypeError(`Index already exists: ${index.name}`);
|
|
1387
|
+
indexNames.add(index.name);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1199
1390
|
for (const segment of snapshot.segments) {
|
|
1200
1391
|
this.#segments.set(segment.id, normalizeSegmentRecord(segment));
|
|
1201
1392
|
}
|
|
@@ -1213,6 +1404,13 @@ export class RecordCore {
|
|
|
1213
1404
|
if (table.uniqueKeyTokens.length > 0) {
|
|
1214
1405
|
this.#uniqueKeys.set(record.id, new Set(table.uniqueKeyTokens));
|
|
1215
1406
|
}
|
|
1407
|
+
for (const entry of table.secondaryUniqueKeys ?? []) {
|
|
1408
|
+
const index = record.secondaryIndexes?.[entry.indexId];
|
|
1409
|
+
if (index?.unique !== true) {
|
|
1410
|
+
throw new TypeError(`Snapshot has membership for an unknown UNIQUE index: ${entry.indexId}`);
|
|
1411
|
+
}
|
|
1412
|
+
this.#uniqueKeys.set(secondaryUniqueKeyNamespace(record.id, entry.indexId), new Set(entry.keyTokens));
|
|
1413
|
+
}
|
|
1216
1414
|
for (const index of table.fts) {
|
|
1217
1415
|
this.#ftsBases.set(`${record.id}/${index.columnId}`, {
|
|
1218
1416
|
coversVersion: index.coversVersion,
|
|
@@ -1266,6 +1464,16 @@ export class RecordCore {
|
|
|
1266
1464
|
/** Replaces the whole record state with a dump's content. */
|
|
1267
1465
|
load(state) {
|
|
1268
1466
|
const cloned = structuredClone(state);
|
|
1467
|
+
const indexNames = new Set();
|
|
1468
|
+
for (const table of cloned.tables) {
|
|
1469
|
+
validateTableColumns(table.columns);
|
|
1470
|
+
validateSecondaryIndexes(table);
|
|
1471
|
+
for (const index of Object.values(table.secondaryIndexes ?? {})) {
|
|
1472
|
+
if (indexNames.has(index.name))
|
|
1473
|
+
throw new TypeError(`Index already exists: ${index.name}`);
|
|
1474
|
+
indexNames.add(index.name);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1269
1477
|
for (const map of [
|
|
1270
1478
|
this.#manifests,
|
|
1271
1479
|
this.#transactions,
|
|
@@ -1435,8 +1643,12 @@ function assertGarbageCollectionCandidateProvenance(job, manifests, segments, tr
|
|
|
1435
1643
|
}
|
|
1436
1644
|
}
|
|
1437
1645
|
const blockHasProvenance = (id) => {
|
|
1438
|
-
|
|
1439
|
-
|
|
1646
|
+
// A prior collection pass may already have pruned the manifest that names a leftover block.
|
|
1647
|
+
// Its persisted record is still provenance until the block is reclaimed; requiring that
|
|
1648
|
+
// already-pruned version to appear in candidateManifestVersions is impossible because that
|
|
1649
|
+
// list contains only manifests this job will newly prune.
|
|
1650
|
+
for (const manifest of manifests.values()) {
|
|
1651
|
+
if (manifest.blockIds.includes(id))
|
|
1440
1652
|
return true;
|
|
1441
1653
|
}
|
|
1442
1654
|
for (const transaction of transactions.values()) {
|
|
@@ -1456,20 +1668,10 @@ function assertGarbageCollectionCandidateProvenance(job, manifests, segments, tr
|
|
|
1456
1668
|
throw new Error(`Garbage collection block candidate has no persisted provenance: ${unprovenBlockId}`);
|
|
1457
1669
|
}
|
|
1458
1670
|
const unprovenSegmentId = job.candidateSegmentIds.find((id) => {
|
|
1459
|
-
for
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
}
|
|
1464
|
-
for (const compaction of compactionJobs.values()) {
|
|
1465
|
-
if (isTerminalCompactionJob(compaction) &&
|
|
1466
|
-
(compaction.sourceSegmentIds.includes(id) || compaction.outputSegmentId === id)) {
|
|
1467
|
-
return false;
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
const segment = segments.get(id);
|
|
1471
|
-
const blockIds = segment === undefined ? [] : segmentBlockIds(segment);
|
|
1472
|
-
return blockIds.length === 0 || !blockIds.every(blockHasProvenance);
|
|
1671
|
+
// The persisted segment record is sufficient provenance for nominating that exact id.
|
|
1672
|
+
// Its blocks may already have been reclaimed by an earlier bounded pass, and terminal
|
|
1673
|
+
// compaction records are intentionally aged out, so neither is a durable discovery source.
|
|
1674
|
+
return !segments.has(id);
|
|
1473
1675
|
});
|
|
1474
1676
|
if (unprovenSegmentId !== undefined) {
|
|
1475
1677
|
throw new Error(`Garbage collection segment candidate has no persisted provenance: ${unprovenSegmentId}`);
|
|
@@ -1510,29 +1712,25 @@ function collectBoundedPhysicalRoots(candidateSegmentIds, candidateBlockIds, man
|
|
|
1510
1712
|
const candidateBlocks = new Set(candidateBlockIds);
|
|
1511
1713
|
const probeBlockIds = new Set(candidateBlockIds);
|
|
1512
1714
|
const probeSegmentIds = new Set(candidateSegmentIds);
|
|
1513
|
-
|
|
1514
|
-
|
|
1715
|
+
// First discover a bounded working set. A very wide segment can cross the cache ceiling; its
|
|
1716
|
+
// remaining IDs are checked lazily below instead of retaining the whole batch forever.
|
|
1515
1717
|
for (const segment of segments.values()) {
|
|
1516
1718
|
const ids = segmentBlockIds(segment);
|
|
1517
1719
|
if (!candidateSegmentIds.includes(segment.id) && !ids.some((id) => candidateBlocks.has(id))) {
|
|
1518
1720
|
continue;
|
|
1519
1721
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1722
|
+
if (probeSegmentIds.size < MAX_GARBAGE_COLLECTION_ROOT_DEPENDENCIES) {
|
|
1723
|
+
probeSegmentIds.add(segment.id);
|
|
1724
|
+
}
|
|
1725
|
+
for (const id of ids) {
|
|
1726
|
+
if (probeBlockIds.size >= MAX_GARBAGE_COLLECTION_ROOT_DEPENDENCIES)
|
|
1727
|
+
break;
|
|
1728
|
+
probeBlockIds.add(id);
|
|
1525
1729
|
}
|
|
1526
|
-
relatedSegments.set(segment.id, segment);
|
|
1527
|
-
probeSegmentIds.add(segment.id);
|
|
1528
|
-
ids.forEach((id) => probeBlockIds.add(id));
|
|
1529
|
-
}
|
|
1530
|
-
if (dependencyOverflow) {
|
|
1531
|
-
return { blockIds: new Set(candidateBlockIds), segmentIds: new Set(candidateSegmentIds) };
|
|
1532
1730
|
}
|
|
1533
1731
|
const directBlockRoots = new Set();
|
|
1534
1732
|
const directSegmentRoots = new Set();
|
|
1535
|
-
for (const manifest of manifests) {
|
|
1733
|
+
for (const manifest of manifests.values()) {
|
|
1536
1734
|
if (manifest.prunedAt !== undefined || newlyPrunedVersions.has(manifest.version))
|
|
1537
1735
|
continue;
|
|
1538
1736
|
for (const id of manifest.blockIds)
|
|
@@ -1549,7 +1747,7 @@ function collectBoundedPhysicalRoots(candidateSegmentIds, candidateBlockIds, man
|
|
|
1549
1747
|
if (probeSegmentIds.has(id))
|
|
1550
1748
|
directSegmentRoots.add(id);
|
|
1551
1749
|
}
|
|
1552
|
-
for (const job of compactionJobs) {
|
|
1750
|
+
for (const job of compactionJobs.values()) {
|
|
1553
1751
|
if (isTerminalCompactionJob(job))
|
|
1554
1752
|
continue;
|
|
1555
1753
|
for (const id of job.sourceBlockIds)
|
|
@@ -1565,13 +1763,82 @@ function collectBoundedPhysicalRoots(candidateSegmentIds, candidateBlockIds, man
|
|
|
1565
1763
|
directSegmentRoots.add(job.outputSegmentId);
|
|
1566
1764
|
}
|
|
1567
1765
|
}
|
|
1766
|
+
const uncachedBlockRoots = new Map();
|
|
1767
|
+
const isDirectBlockRoot = (id) => {
|
|
1768
|
+
if (probeBlockIds.has(id))
|
|
1769
|
+
return directBlockRoots.has(id);
|
|
1770
|
+
const cached = uncachedBlockRoots.get(id);
|
|
1771
|
+
if (cached !== undefined)
|
|
1772
|
+
return cached;
|
|
1773
|
+
let rooted = false;
|
|
1774
|
+
for (const manifest of manifests.values()) {
|
|
1775
|
+
if (manifest.prunedAt === undefined &&
|
|
1776
|
+
!newlyPrunedVersions.has(manifest.version) &&
|
|
1777
|
+
manifest.blockIds.includes(id)) {
|
|
1778
|
+
rooted = true;
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (!rooted) {
|
|
1783
|
+
for (const transaction of transactions.values()) {
|
|
1784
|
+
if (transaction.status === "active" && transaction.pendingBlockIds.includes(id)) {
|
|
1785
|
+
rooted = true;
|
|
1786
|
+
break;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (!rooted) {
|
|
1791
|
+
for (const job of compactionJobs.values()) {
|
|
1792
|
+
if (!isTerminalCompactionJob(job) &&
|
|
1793
|
+
(job.sourceBlockIds.includes(id) || job.outputBlockIds.includes(id))) {
|
|
1794
|
+
rooted = true;
|
|
1795
|
+
break;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (uncachedBlockRoots.size < MAX_GARBAGE_COLLECTION_ROOT_DEPENDENCIES) {
|
|
1800
|
+
uncachedBlockRoots.set(id, rooted);
|
|
1801
|
+
}
|
|
1802
|
+
return rooted;
|
|
1803
|
+
};
|
|
1804
|
+
const uncachedSegmentRoots = new Map();
|
|
1805
|
+
const isDirectSegmentRoot = (id) => {
|
|
1806
|
+
if (probeSegmentIds.has(id))
|
|
1807
|
+
return directSegmentRoots.has(id);
|
|
1808
|
+
const cached = uncachedSegmentRoots.get(id);
|
|
1809
|
+
if (cached !== undefined)
|
|
1810
|
+
return cached;
|
|
1811
|
+
let rooted = false;
|
|
1812
|
+
for (const transaction of transactions.values()) {
|
|
1813
|
+
if (transaction.status === "active" && transaction.pendingSegmentIds.includes(id)) {
|
|
1814
|
+
rooted = true;
|
|
1815
|
+
break;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (!rooted) {
|
|
1819
|
+
for (const job of compactionJobs.values()) {
|
|
1820
|
+
if (!isTerminalCompactionJob(job) &&
|
|
1821
|
+
(job.sourceSegmentIds.includes(id) || job.outputSegmentId === id)) {
|
|
1822
|
+
rooted = true;
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
if (uncachedSegmentRoots.size < MAX_GARBAGE_COLLECTION_ROOT_DEPENDENCIES) {
|
|
1828
|
+
uncachedSegmentRoots.set(id, rooted);
|
|
1829
|
+
}
|
|
1830
|
+
return rooted;
|
|
1831
|
+
};
|
|
1568
1832
|
const rootedBlockIds = new Set();
|
|
1569
1833
|
const rootedSegmentIds = new Set();
|
|
1570
|
-
for (const segment of
|
|
1834
|
+
for (const segment of segments.values()) {
|
|
1571
1835
|
const ids = segmentBlockIds(segment);
|
|
1572
|
-
if (
|
|
1836
|
+
if (!candidateSegmentIds.includes(segment.id) && !ids.some((id) => candidateBlocks.has(id))) {
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
if (isDirectSegmentRoot(segment.id) ||
|
|
1573
1840
|
transactions.get(segment.transactionId)?.status === "active" ||
|
|
1574
|
-
(ids.length > 0 && ids.every(
|
|
1841
|
+
(ids.length > 0 && ids.every(isDirectBlockRoot))) {
|
|
1575
1842
|
if (candidateSegmentIds.includes(segment.id))
|
|
1576
1843
|
rootedSegmentIds.add(segment.id);
|
|
1577
1844
|
for (const id of ids)
|
|
@@ -1580,10 +1847,10 @@ function collectBoundedPhysicalRoots(candidateSegmentIds, candidateBlockIds, man
|
|
|
1580
1847
|
}
|
|
1581
1848
|
}
|
|
1582
1849
|
for (const id of candidateBlockIds)
|
|
1583
|
-
if (
|
|
1850
|
+
if (isDirectBlockRoot(id))
|
|
1584
1851
|
rootedBlockIds.add(id);
|
|
1585
1852
|
for (const id of candidateSegmentIds)
|
|
1586
|
-
if (
|
|
1853
|
+
if (isDirectSegmentRoot(id))
|
|
1587
1854
|
rootedSegmentIds.add(id);
|
|
1588
1855
|
return { blockIds: rootedBlockIds, segmentIds: rootedSegmentIds };
|
|
1589
1856
|
}
|
|
@@ -1608,14 +1875,22 @@ function validatePageLimit(limit) {
|
|
|
1608
1875
|
throw new RangeError("Storage page limit must be a positive whole number");
|
|
1609
1876
|
}
|
|
1610
1877
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1878
|
+
/** Selects the next sorted page without copying or sorting the complete backing map. */
|
|
1879
|
+
function boundedRecordPage(values, after, limit, keyOf) {
|
|
1880
|
+
const selected = [];
|
|
1881
|
+
for (const value of values) {
|
|
1882
|
+
const key = keyOf(value);
|
|
1883
|
+
if (after !== null && key <= after)
|
|
1884
|
+
continue;
|
|
1885
|
+
const index = selected.findIndex((entry) => entry.key >= key);
|
|
1886
|
+
if (index < 0)
|
|
1887
|
+
selected.push({ key, value });
|
|
1888
|
+
else
|
|
1889
|
+
selected.splice(index, 0, { key, value });
|
|
1890
|
+
if (selected.length > limit)
|
|
1891
|
+
selected.pop();
|
|
1892
|
+
}
|
|
1893
|
+
return selected.map(({ value }) => value);
|
|
1619
1894
|
}
|
|
1620
1895
|
function validateTempOwnerRecord(record) {
|
|
1621
1896
|
validateId(record.ownerId);
|