@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
package/dist/storage/types.js
CHANGED
|
@@ -25,6 +25,29 @@ export function applyManifestRecord(blockIds, record) {
|
|
|
25
25
|
blockIds.add(id);
|
|
26
26
|
}
|
|
27
27
|
export const simpleDataTypes = ["boolean", "number", "string", "datetime"];
|
|
28
|
+
/** Validates the logical metadata shared by SQL DDL, the schema DSL, and restored catalogs. */
|
|
29
|
+
export function validateSqlDomain(domain, context) {
|
|
30
|
+
if (domain.kind === "numeric") {
|
|
31
|
+
const { precision, scale } = domain;
|
|
32
|
+
if ((precision !== undefined &&
|
|
33
|
+
(!Number.isSafeInteger(precision) || precision < 1 || precision > 100_000)) ||
|
|
34
|
+
(scale !== undefined && (!Number.isSafeInteger(scale) || scale < 0 || scale > 100_000)) ||
|
|
35
|
+
(precision !== undefined && scale !== undefined && scale > precision)) {
|
|
36
|
+
throw new TypeError(`Invalid NUMERIC domain metadata: ${context}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (domain.kind === "array" &&
|
|
40
|
+
(domain.element.trim().length === 0 || domain.element.trim() !== domain.element)) {
|
|
41
|
+
throw new TypeError(`ARRAY element type must be a trimmed non-empty name: ${context}`);
|
|
42
|
+
}
|
|
43
|
+
if (domain.kind === "enum") {
|
|
44
|
+
if (domain.name.trim().length === 0 || domain.name.trim() !== domain.name) {
|
|
45
|
+
throw new TypeError(`Enum type name must be a trimmed non-empty name: ${context}`);
|
|
46
|
+
}
|
|
47
|
+
validateEnumValues(domain.values, domain.name);
|
|
48
|
+
}
|
|
49
|
+
return structuredClone(domain);
|
|
50
|
+
}
|
|
28
51
|
/**
|
|
29
52
|
* The single authority on which enum declarations are legal, shared by the schema DSL's
|
|
30
53
|
* `column.enum()` and the engine's `createTable`: at least one value, every value a non-empty
|
|
@@ -46,22 +69,45 @@ export function validateEnumValues(values, context) {
|
|
|
46
69
|
}
|
|
47
70
|
return [...values];
|
|
48
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Validates the catalog invariants owned by a table's column list. Storage adapters call this
|
|
74
|
+
* for ordinary catalog writes and before restoring snapshots or checkpoints, so malformed
|
|
75
|
+
* metadata cannot enter through a less common persistence path.
|
|
76
|
+
*/
|
|
77
|
+
export function validateTableColumns(columns) {
|
|
78
|
+
if (columns.length === 0)
|
|
79
|
+
throw new TypeError("A table needs at least one column");
|
|
80
|
+
for (const column of columns) {
|
|
81
|
+
const integer = column.integer;
|
|
82
|
+
if (integer !== undefined && (integer !== true || column.type !== "number")) {
|
|
83
|
+
throw new TypeError(`Integer domain requires a number column: ${column.name}`);
|
|
84
|
+
}
|
|
85
|
+
if (column.sqlDomain !== undefined && column.type !== "string") {
|
|
86
|
+
throw new TypeError(`SQL domain columns must use string storage: ${column.name}`);
|
|
87
|
+
}
|
|
88
|
+
if (column.sqlDomain !== undefined)
|
|
89
|
+
validateSqlDomain(column.sqlDomain, column.name);
|
|
90
|
+
if (column.integer === true && column.sqlDomain !== undefined) {
|
|
91
|
+
throw new TypeError(`A column cannot have both integer and SQL-domain metadata: ${column.name}`);
|
|
92
|
+
}
|
|
93
|
+
if (column.enumValues !== undefined && column.sqlDomain !== undefined) {
|
|
94
|
+
throw new TypeError(`A column cannot have both enum restrictions and a SQL domain: ${column.name}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const ids = new Set(columns.map(({ id }) => id));
|
|
98
|
+
const names = new Set(columns.map(({ name }) => name));
|
|
99
|
+
if (ids.size !== columns.length || names.size !== columns.length) {
|
|
100
|
+
throw new TypeError("Table columns must have unique IDs and names");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
49
103
|
/**
|
|
50
104
|
* The single authority on which default declarations are legal, shared by the schema DSL's
|
|
51
105
|
* `table()` and the engine's `createTable` so the two entry points (and the wire path between
|
|
52
|
-
* them) can never drift
|
|
53
|
-
*
|
|
106
|
+
* them) can never drift. Storage owns structural and literal validation; the engine additionally
|
|
107
|
+
* parses and type-checks SQL expressions before catalog mutation.
|
|
54
108
|
*/
|
|
55
109
|
export function validateColumnDefault(column, defaultValue) {
|
|
56
|
-
if (column.nullable) {
|
|
57
|
-
throw new TypeError(`Defaults require a non-nullable column: ${column.name}`);
|
|
58
|
-
}
|
|
59
110
|
switch (defaultValue.kind) {
|
|
60
|
-
case "now":
|
|
61
|
-
if (column.type !== "datetime") {
|
|
62
|
-
throw new TypeError(`Default now requires a datetime column: ${column.name}`);
|
|
63
|
-
}
|
|
64
|
-
return;
|
|
65
111
|
case "autoincrement":
|
|
66
112
|
if (column.type !== "number") {
|
|
67
113
|
throw new TypeError(`Auto-increment requires a number column: ${column.name}`);
|
|
@@ -70,32 +116,110 @@ export function validateColumnDefault(column, defaultValue) {
|
|
|
70
116
|
throw new TypeError(`Auto-increment requires the unique key column: ${column.name}`);
|
|
71
117
|
}
|
|
72
118
|
return;
|
|
119
|
+
case "expression":
|
|
120
|
+
if (typeof defaultValue.sql !== "string" ||
|
|
121
|
+
defaultValue.sql.length === 0 ||
|
|
122
|
+
defaultValue.sql.trim() !== defaultValue.sql) {
|
|
123
|
+
throw new TypeError(`Default SQL must be a trimmed non-empty expression: ${column.name}`);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
73
126
|
case "literal": {
|
|
74
127
|
const value = defaultValue.value;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
128
|
+
const numericDomain = column.sqlDomain?.kind === "numeric";
|
|
129
|
+
const correctType = column.type === "datetime"
|
|
130
|
+
? value instanceof Date && Number.isFinite(value.getTime())
|
|
131
|
+
: numericDomain
|
|
132
|
+
? typeof value === "number" || typeof value === "string"
|
|
133
|
+
: typeof value === column.type;
|
|
134
|
+
if (!correctType) {
|
|
79
135
|
throw new TypeError(`Default literal must be a ${column.type}: ${column.name}`);
|
|
80
136
|
}
|
|
81
137
|
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
82
138
|
throw new TypeError(`Default literal must be finite: ${column.name}`);
|
|
83
139
|
}
|
|
140
|
+
if (column.integer === true && !Number.isSafeInteger(value)) {
|
|
141
|
+
throw new TypeError(`Default literal must be a safe integer: ${column.name}`);
|
|
142
|
+
}
|
|
84
143
|
if (column.enumValues !== undefined &&
|
|
85
144
|
typeof value === "string" &&
|
|
86
145
|
!column.enumValues.includes(value)) {
|
|
87
146
|
throw new TypeError(`Default must be one of the enum values: ${column.name}`);
|
|
88
147
|
}
|
|
89
|
-
if (column.
|
|
90
|
-
|
|
148
|
+
if (column.sqlDomain?.kind === "enum" &&
|
|
149
|
+
typeof value === "string" &&
|
|
150
|
+
!column.sqlDomain.values.includes(value)) {
|
|
151
|
+
throw new TypeError(`Default must be one of the enum values: ${column.name}`);
|
|
91
152
|
}
|
|
92
153
|
return;
|
|
93
154
|
}
|
|
94
|
-
//
|
|
155
|
+
// Storage restoration and untyped wire callers can still supply malformed catalog data.
|
|
95
156
|
default:
|
|
96
157
|
throw new TypeError(`Unknown default kind: ${String(defaultValue.kind)}`);
|
|
97
158
|
}
|
|
98
159
|
}
|
|
160
|
+
/** Validates the durable identities and ownership rules of one table's secondary indexes. */
|
|
161
|
+
export function validateSecondaryIndexes(record) {
|
|
162
|
+
const columnIds = new Set(record.columns.map((column) => column.id));
|
|
163
|
+
const names = new Set();
|
|
164
|
+
const storageIds = new Set(Object.keys(record.ftsColumns ?? {}));
|
|
165
|
+
for (const [indexId, index] of Object.entries(record.secondaryIndexes ?? {})) {
|
|
166
|
+
if (indexId.length === 0 || index.name.length === 0 || index.storageColumnId.length === 0) {
|
|
167
|
+
throw new TypeError("Secondary-index IDs and names must be non-empty");
|
|
168
|
+
}
|
|
169
|
+
const indexedColumnIds = index.columnIds ?? [index.columnId];
|
|
170
|
+
if (indexedColumnIds.length === 0 ||
|
|
171
|
+
indexedColumnIds[0] !== index.columnId ||
|
|
172
|
+
new Set(indexedColumnIds).size !== indexedColumnIds.length ||
|
|
173
|
+
indexedColumnIds.some((columnId) => !columnIds.has(columnId))) {
|
|
174
|
+
throw new TypeError(`Secondary index ${index.name} references an unknown column`);
|
|
175
|
+
}
|
|
176
|
+
const directions = index.directions ?? indexedColumnIds.map(() => "asc");
|
|
177
|
+
const termEncoding = index.termEncoding;
|
|
178
|
+
if (directions.length !== indexedColumnIds.length ||
|
|
179
|
+
directions.some((direction) => direction !== "asc" && direction !== "desc") ||
|
|
180
|
+
(termEncoding !== undefined && termEncoding !== "tuple-v1") ||
|
|
181
|
+
(termEncoding !== "tuple-v1" &&
|
|
182
|
+
(indexedColumnIds.length > 1 ||
|
|
183
|
+
directions.some((direction) => direction !== "asc") ||
|
|
184
|
+
index.unique === true))) {
|
|
185
|
+
throw new TypeError(`Secondary index ${index.name} has invalid key metadata`);
|
|
186
|
+
}
|
|
187
|
+
if (index.uniqueEnforced === true && index.unique !== true) {
|
|
188
|
+
throw new TypeError(`Secondary index ${index.name} enforces uniqueness without UNIQUE`);
|
|
189
|
+
}
|
|
190
|
+
if (names.has(index.name))
|
|
191
|
+
throw new TypeError(`Index already exists: ${index.name}`);
|
|
192
|
+
names.add(index.name);
|
|
193
|
+
if (storageIds.has(index.storageColumnId)) {
|
|
194
|
+
throw new TypeError(`Secondary-index storage ID is already used: ${index.storageColumnId}`);
|
|
195
|
+
}
|
|
196
|
+
storageIds.add(index.storageColumnId);
|
|
197
|
+
const expectedLocator = record.uniqueKeyColumnId === undefined ? "row-id" : "key-hash-v1";
|
|
198
|
+
const storage = index.storage;
|
|
199
|
+
if (storage !== "postings-v1" || index.locator !== expectedLocator) {
|
|
200
|
+
throw new TypeError(`Secondary index ${index.name} has incompatible storage metadata`);
|
|
201
|
+
}
|
|
202
|
+
const state = index.state;
|
|
203
|
+
if ((state !== "building" && state !== "ready" && state !== "invalid") ||
|
|
204
|
+
!Number.isSafeInteger(index.buildFromVersion) ||
|
|
205
|
+
index.buildFromVersion < -1 ||
|
|
206
|
+
(index.state === "building") !== (index.buildId !== undefined)) {
|
|
207
|
+
throw new TypeError(`Secondary index ${index.name} has invalid build metadata`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Ordered catalog column IDs for legacy and current secondary-index records. */
|
|
212
|
+
export function secondaryIndexColumnIds(index) {
|
|
213
|
+
return index.columnIds ?? [index.columnId];
|
|
214
|
+
}
|
|
215
|
+
/** Declared directions for legacy and current secondary-index records. */
|
|
216
|
+
export function secondaryIndexDirections(index) {
|
|
217
|
+
return index.directions ?? secondaryIndexColumnIds(index).map(() => "asc");
|
|
218
|
+
}
|
|
219
|
+
/** Unique-membership namespace owned by one physical secondary index. */
|
|
220
|
+
export function secondaryUniqueKeyNamespace(tableId, indexId) {
|
|
221
|
+
return `${tableId}\u0000secondary-index\u0000${indexId}`;
|
|
222
|
+
}
|
|
99
223
|
export class TableRecordConflictError extends Error {
|
|
100
224
|
tableId;
|
|
101
225
|
expectedRevision;
|
|
@@ -163,6 +287,20 @@ export class LeaseConflictError extends Error {
|
|
|
163
287
|
this.actualRevision = actualRevision;
|
|
164
288
|
}
|
|
165
289
|
}
|
|
290
|
+
/** Whether a stored term belongs to one exact, prefix, or range lookup. */
|
|
291
|
+
export function ftsPostingQueryMatches(term, query) {
|
|
292
|
+
if ("term" in query)
|
|
293
|
+
return query.prefix ? term.startsWith(query.term) : term === query.term;
|
|
294
|
+
if (query.lower !== undefined) {
|
|
295
|
+
if (term < query.lower || (term === query.lower && query.lowerInclusive === false))
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
if (query.upper !== undefined) {
|
|
299
|
+
if (term > query.upper || (term === query.upper && query.upperInclusive === false))
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
166
304
|
/**
|
|
167
305
|
* Shared candidate-merge core for both stores: fetching chunks is store-specific, but the
|
|
168
306
|
* term-match rule (exact, or prefix as a term range) and the sorted-unique row-id shape must
|
|
@@ -176,9 +314,7 @@ export function collectFtsCandidates(chunkLists, terms) {
|
|
|
176
314
|
const term = terms[index];
|
|
177
315
|
if (term === undefined)
|
|
178
316
|
continue;
|
|
179
|
-
const matches = term
|
|
180
|
-
? posting.term.startsWith(term.term)
|
|
181
|
-
: posting.term === term.term;
|
|
317
|
+
const matches = ftsPostingQueryMatches(posting.term, term);
|
|
182
318
|
if (!matches)
|
|
183
319
|
continue;
|
|
184
320
|
const set = sets[index];
|
|
@@ -192,6 +328,97 @@ export function collectFtsCandidates(chunkLists, terms) {
|
|
|
192
328
|
rowIdsByTerm: sets.map((set) => [...set].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))),
|
|
193
329
|
};
|
|
194
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* K-way merges sorted base/delta chunks into canonical term order. Only one term's row map and
|
|
333
|
+
* one cursor per chunk stay live beyond the returned postings, avoiding a second index-sized map.
|
|
334
|
+
*/
|
|
335
|
+
export function collectFtsPostings(chunkLists) {
|
|
336
|
+
const heap = [];
|
|
337
|
+
const before = (left, right) => {
|
|
338
|
+
const leftTerm = left.postings[left.position]?.term ?? "";
|
|
339
|
+
const rightTerm = right.postings[right.position]?.term ?? "";
|
|
340
|
+
return leftTerm < rightTerm || (leftTerm === rightTerm && left.ordinal < right.ordinal);
|
|
341
|
+
};
|
|
342
|
+
const cursorAt = (position) => {
|
|
343
|
+
const cursor = heap[position];
|
|
344
|
+
if (cursor === undefined)
|
|
345
|
+
throw new Error("Posting merge heap is inconsistent");
|
|
346
|
+
return cursor;
|
|
347
|
+
};
|
|
348
|
+
const push = (cursor) => {
|
|
349
|
+
heap.push(cursor);
|
|
350
|
+
let child = heap.length - 1;
|
|
351
|
+
while (child > 0) {
|
|
352
|
+
const parent = (child - 1) >>> 1;
|
|
353
|
+
const childCursor = cursorAt(child);
|
|
354
|
+
const parentCursor = cursorAt(parent);
|
|
355
|
+
if (!before(childCursor, parentCursor))
|
|
356
|
+
break;
|
|
357
|
+
heap[parent] = childCursor;
|
|
358
|
+
heap[child] = parentCursor;
|
|
359
|
+
child = parent;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
const pop = () => {
|
|
363
|
+
const first = heap[0];
|
|
364
|
+
const last = heap.pop();
|
|
365
|
+
if (first === undefined || last === undefined)
|
|
366
|
+
return first;
|
|
367
|
+
if (heap.length === 0)
|
|
368
|
+
return first;
|
|
369
|
+
heap[0] = last;
|
|
370
|
+
let parent = 0;
|
|
371
|
+
for (;;) {
|
|
372
|
+
const left = parent * 2 + 1;
|
|
373
|
+
if (left >= heap.length)
|
|
374
|
+
break;
|
|
375
|
+
const right = left + 1;
|
|
376
|
+
const child = right < heap.length && before(cursorAt(right), cursorAt(left)) ? right : left;
|
|
377
|
+
const childCursor = cursorAt(child);
|
|
378
|
+
const parentCursor = cursorAt(parent);
|
|
379
|
+
if (!before(childCursor, parentCursor))
|
|
380
|
+
break;
|
|
381
|
+
heap[parent] = childCursor;
|
|
382
|
+
heap[child] = parentCursor;
|
|
383
|
+
parent = child;
|
|
384
|
+
}
|
|
385
|
+
return first;
|
|
386
|
+
};
|
|
387
|
+
let ordinal = 0;
|
|
388
|
+
for (const postings of chunkLists) {
|
|
389
|
+
if (postings.length > 0)
|
|
390
|
+
push({ postings, position: 0, ordinal });
|
|
391
|
+
ordinal += 1;
|
|
392
|
+
}
|
|
393
|
+
const output = [];
|
|
394
|
+
while (heap.length > 0) {
|
|
395
|
+
const term = heap[0]?.postings[heap[0].position]?.term;
|
|
396
|
+
if (term === undefined)
|
|
397
|
+
break;
|
|
398
|
+
const rows = new Map();
|
|
399
|
+
while (heap[0]?.postings[heap[0].position]?.term === term) {
|
|
400
|
+
const cursor = pop();
|
|
401
|
+
if (cursor === undefined)
|
|
402
|
+
break;
|
|
403
|
+
const posting = cursor.postings[cursor.position];
|
|
404
|
+
if (posting === undefined)
|
|
405
|
+
continue;
|
|
406
|
+
posting.rowIds.forEach((rowId, index) => {
|
|
407
|
+
rows.set(rowId, Math.max(rows.get(rowId) ?? 0, posting.tf[index] ?? 1));
|
|
408
|
+
});
|
|
409
|
+
cursor.position += 1;
|
|
410
|
+
if (cursor.position < cursor.postings.length)
|
|
411
|
+
push(cursor);
|
|
412
|
+
}
|
|
413
|
+
const ordered = [...rows].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
414
|
+
output.push({
|
|
415
|
+
term,
|
|
416
|
+
rowIds: ordered.map(([rowId]) => rowId),
|
|
417
|
+
tf: ordered.map(([, frequency]) => frequency),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return output;
|
|
421
|
+
}
|
|
195
422
|
/**
|
|
196
423
|
* Shared stale-writer policy for both stores' commit steps: a commit that adds segments to a
|
|
197
424
|
* table with active full-text columns but no covering delta flips the uncovered columns to
|
|
@@ -217,6 +444,39 @@ export function invalidateUncoveredFtsColumns(record, coveredColumnIds) {
|
|
|
217
444
|
return undefined;
|
|
218
445
|
return { ...record, ftsColumns: next, revision: (record.revision ?? 0) + 1 };
|
|
219
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Scalar-index half of the stale-writer rule. A writer that staged table data from catalog
|
|
449
|
+
* metadata older than a newly building/ready index cannot provide its postings, so the commit
|
|
450
|
+
* invalidates that index atomically. Readers then scan until a rebuild closes the gap.
|
|
451
|
+
*/
|
|
452
|
+
export function invalidateUncoveredSecondaryIndexes(record, coveredStorageColumnIds) {
|
|
453
|
+
const indexes = record.secondaryIndexes;
|
|
454
|
+
if (indexes === undefined)
|
|
455
|
+
return undefined;
|
|
456
|
+
let invalidated = false;
|
|
457
|
+
const next = {};
|
|
458
|
+
for (const [indexId, index] of Object.entries(indexes)) {
|
|
459
|
+
if (index.state !== "invalid" && !coveredStorageColumnIds.has(index.storageColumnId)) {
|
|
460
|
+
const { buildId: _abandonedBuild, ...invalid } = index;
|
|
461
|
+
void _abandonedBuild;
|
|
462
|
+
next[indexId] = { ...invalid, state: "invalid" };
|
|
463
|
+
invalidated = true;
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
next[indexId] = { ...index };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!invalidated)
|
|
470
|
+
return undefined;
|
|
471
|
+
return { ...record, secondaryIndexes: next, revision: (record.revision ?? 0) + 1 };
|
|
472
|
+
}
|
|
473
|
+
/** Physical posting IDs a current catalog record authorizes a commit to write. */
|
|
474
|
+
export function activePostingStorageColumnIds(record) {
|
|
475
|
+
return new Set([
|
|
476
|
+
...Object.entries(record.ftsColumns ?? {}).flatMap(([columnId, index]) => index.state === "invalid" ? [] : [columnId]),
|
|
477
|
+
...Object.values(record.secondaryIndexes ?? {}).flatMap((index) => index.state === "invalid" ? [] : [index.storageColumnId]),
|
|
478
|
+
]);
|
|
479
|
+
}
|
|
220
480
|
export class UniqueKeyConflictError extends Error {
|
|
221
481
|
tableId;
|
|
222
482
|
keyToken;
|
|
@@ -227,6 +487,17 @@ export class UniqueKeyConflictError extends Error {
|
|
|
227
487
|
this.keyToken = keyToken;
|
|
228
488
|
}
|
|
229
489
|
}
|
|
490
|
+
/** A writer prepared before an enforced UNIQUE index existed and therefore cannot enforce it. */
|
|
491
|
+
export class UniqueIndexCoverageError extends Error {
|
|
492
|
+
tableId;
|
|
493
|
+
indexName;
|
|
494
|
+
name = "UniqueIndexCoverageError";
|
|
495
|
+
constructor(tableId, indexName) {
|
|
496
|
+
super(`Write did not cover enforced UNIQUE index ${indexName} on table ${tableId}`);
|
|
497
|
+
this.tableId = tableId;
|
|
498
|
+
this.indexName = indexName;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
230
501
|
export class WriteConflictError extends Error {
|
|
231
502
|
expectedVersion;
|
|
232
503
|
actualVersion;
|