@minnowdb/core 0.5.0 → 0.6.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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/engine/cancellation.d.ts +2 -0
  3. package/dist/engine/cancellation.js +4 -0
  4. package/dist/engine/catalog.d.ts +3 -1
  5. package/dist/engine/catalog.js +1 -0
  6. package/dist/engine/client.d.ts +32 -4
  7. package/dist/engine/client.js +82 -15
  8. package/dist/engine/database.d.ts +23 -14
  9. package/dist/engine/database.js +516 -74
  10. package/dist/engine/defaults.js +11 -0
  11. package/dist/engine/errors.d.ts +13 -0
  12. package/dist/engine/errors.js +22 -0
  13. package/dist/engine/fts.d.ts +2 -15
  14. package/dist/engine/live.d.ts +1 -7
  15. package/dist/engine/live.js +2 -12
  16. package/dist/engine/optimizer.js +540 -38
  17. package/dist/engine/query.d.ts +5 -278
  18. package/dist/engine/query.js +78 -32
  19. package/dist/engine/schema-wire.d.ts +7 -1
  20. package/dist/engine/schema-wire.js +4 -0
  21. package/dist/engine/schema.d.ts +67 -33
  22. package/dist/engine/schema.js +138 -7
  23. package/dist/engine/sql-domains.d.ts +8 -0
  24. package/dist/engine/sql-domains.js +25 -0
  25. package/dist/engine/sql-json.js +22 -3
  26. package/dist/engine/vector.d.ts +2 -2
  27. package/dist/engine/vector.js +301 -39
  28. package/dist/engine/worker-host.js +119 -44
  29. package/dist/plan/index.d.ts +5 -4
  30. package/dist/plan/index.js +3 -3
  31. package/dist/plan/model.d.ts +218 -0
  32. package/dist/plan/model.js +1 -0
  33. package/dist/storage/types.d.ts +7 -0
  34. package/dist/storage/types.js +16 -0
  35. package/dist/transactions/index.d.ts +5 -3
  36. package/dist/transactions/index.js +58 -8
  37. package/dist/worker-protocol/index.d.ts +6 -1
  38. package/dist/worker-protocol/index.js +5 -2
  39. package/package.json +1 -1
  40. package/sql-feature-matrix.json +69 -19
@@ -12,6 +12,54 @@ const stageOps = ["insertBatch", "upsertBatch", "updateBatch", "deleteBatch"];
12
12
  function isStageOp(value) {
13
13
  return typeof value === "string" && stageOps.includes(value);
14
14
  }
15
+ /**
16
+ * Root methods whose arguments and results are already structured-clone-safe. `satisfies` makes
17
+ * a removed or renamed database method a compile error, while the runtime guard below fails
18
+ * closed if a caller supplies a malformed database object through the manual host seam.
19
+ */
20
+ const directRootMethods = [
21
+ "createTable",
22
+ "createView",
23
+ "dropView",
24
+ "dropColumn",
25
+ "dropTable",
26
+ "createIndex",
27
+ "dropIndex",
28
+ "buildFtsIndex",
29
+ "introspect",
30
+ "listTables",
31
+ "insertBatch",
32
+ "insert",
33
+ "upsertBatch",
34
+ "upsert",
35
+ "updateBatch",
36
+ "update",
37
+ "deleteBatch",
38
+ "delete",
39
+ "runStatement",
40
+ "explain",
41
+ "execute",
42
+ "listVisibleSegmentPage",
43
+ "cleanupQuerySpill",
44
+ "compactTable",
45
+ "compactTableStep",
46
+ "resumeCompactionJob",
47
+ "listCompactionJobs",
48
+ "cancelCompactionJob",
49
+ "collectGarbage",
50
+ "collectGarbageStep",
51
+ "resumeGarbageCollectionJob",
52
+ "bufferPoolStats",
53
+ "maintenanceStatus",
54
+ "checkIntegrity",
55
+ "storageStats",
56
+ "inspectInterruptedImport",
57
+ "abortInterruptedImport",
58
+ "listGarbageCollectionJobs",
59
+ ];
60
+ function isDirectRootMethod(value) {
61
+ return directRootMethods.includes(value);
62
+ }
15
63
  /**
16
64
  * A call result that travels as a columnar frame with its buffers transferred. Only query
17
65
  * results take this path: rows of objects are what structured clone is slowest at, so they are
@@ -120,10 +168,17 @@ class SnapshotImportChunkQueue {
120
168
  function snapshotQueueError(value) {
121
169
  return value instanceof Error ? value : new Error("Snapshot transfer failed", { cause: value });
122
170
  }
171
+ function abortError(message) {
172
+ const error = new Error(message);
173
+ error.name = "AbortError";
174
+ return error;
175
+ }
123
176
  /** Hard per-connection ceiling for resident worker resources abandoned by a client. */
124
177
  export const MAX_WORKER_HANDLES_PER_CONNECTION = 256;
125
178
  const DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS = 30_000;
126
179
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
180
+ /** Shared by RPC methods that do not expose cancellation, avoiding one controller per call. */
181
+ const passiveRpcSignal = new AbortController().signal;
127
182
  class DatabaseRpcServer {
128
183
  database;
129
184
  scope;
@@ -132,6 +187,7 @@ class DatabaseRpcServer {
132
187
  #reservedHandleIds = new Set();
133
188
  #openingHandlePromises = new Set();
134
189
  #settlingWritePromises = new Set();
190
+ #requestAborts = new Map();
135
191
  #disposed = false;
136
192
  #inFlightRpcCount = 0;
137
193
  #inFlightRpcDrain;
@@ -154,6 +210,12 @@ class DatabaseRpcServer {
154
210
  this.scope.postMessage(rpcResult(request.requestId, { ready: true }));
155
211
  return;
156
212
  }
213
+ if (request.kind === "rpc-cancel") {
214
+ this.#requestAborts
215
+ .get(request.requestId)
216
+ ?.abort(abortError("Database request was cancelled"));
217
+ return;
218
+ }
157
219
  const bypassLimit = request.method === "dispose";
158
220
  if (!bypassLimit && this.#inFlightRpcCount >= MAX_DATABASE_RPC_IN_FLIGHT) {
159
221
  this.scope.postMessage(rpcFailure(request.requestId, new RangeError(`A database worker connection cannot hold more than ${String(MAX_DATABASE_RPC_IN_FLIGHT)} in-flight requests`)));
@@ -161,12 +223,19 @@ class DatabaseRpcServer {
161
223
  }
162
224
  if (!bypassLimit)
163
225
  this.#inFlightRpcCount += 1;
226
+ const abort = request.method === "query" ? new AbortController() : undefined;
227
+ if (abort !== undefined)
228
+ this.#requestAborts.set(request.requestId, abort);
229
+ const context = {
230
+ requestId: request.requestId,
231
+ signal: abort?.signal ?? passiveRpcSignal,
232
+ };
164
233
  try {
165
234
  if (this.#disposed)
166
235
  throw new Error("Database connection is disposed");
167
236
  const result = request.handleId === null
168
- ? await this.#callRoot(request.method, request.args)
169
- : await this.#callHandle(request.handleId, request.method, request.args);
237
+ ? await this.#callRoot(request.method, request.args, context)
238
+ : await this.#callHandle(request.handleId, request.method, request.args, context);
170
239
  if (result instanceof ColumnarResult) {
171
240
  this.scope.postMessage(rpcResult(request.requestId, result.encoded.payload), {
172
241
  transfer: result.encoded.transfer,
@@ -190,6 +259,8 @@ class DatabaseRpcServer {
190
259
  this.scope.postMessage(rpcFailure(request.requestId, error));
191
260
  }
192
261
  finally {
262
+ if (abort !== undefined)
263
+ this.#requestAborts.delete(request.requestId);
193
264
  if (!bypassLimit)
194
265
  this.#inFlightRpcCount -= 1;
195
266
  if (this.#inFlightRpcCount === 0 && this.#resolveInFlightRpcDrain !== undefined) {
@@ -200,50 +271,44 @@ class DatabaseRpcServer {
200
271
  }
201
272
  }
202
273
  }
203
- async #callRoot(method, args) {
274
+ async #callRoot(method, args, context) {
204
275
  const database = this.database;
276
+ if (isDirectRootMethod(method)) {
277
+ const operation = database[method];
278
+ if (operation === undefined) {
279
+ throw new Error(`Database root method is unavailable: ${method}`);
280
+ }
281
+ return operation.apply(this.database, args);
282
+ }
205
283
  switch (method) {
206
- case "createTable":
207
- case "introspect":
208
- case "listTables":
209
- case "insertBatch":
210
- case "insert":
211
- case "upsertBatch":
212
- case "upsert":
213
- case "updateBatch":
214
- case "update":
215
- case "deleteBatch":
216
- case "delete":
217
- case "runStatement":
218
- case "explain":
219
- case "execute":
220
- case "listVisibleSegmentPage":
221
- case "cleanupQuerySpill":
222
- case "compactTable":
223
- case "compactTableStep":
224
- case "resumeCompactionJob":
225
- case "listCompactionJobs":
226
- case "cancelCompactionJob":
227
- case "collectGarbage":
228
- case "collectGarbageStep":
229
- case "resumeGarbageCollectionJob":
230
- case "bufferPoolStats":
231
- case "maintenanceStatus":
232
- case "checkIntegrity":
233
- case "storageStats":
234
- case "inspectInterruptedImport":
235
- case "abortInterruptedImport":
236
- case "listGarbageCollectionJobs":
237
- return database[method]?.(...args);
238
284
  case "query": {
239
- const [sql, options] = args;
240
- return new ColumnarResult(encodeQueryResult(await this.database.query(sql, options)));
285
+ const [sql, options, reportStats = false] = args;
286
+ return new ColumnarResult(encodeQueryResult(await this.database.query(sql, {
287
+ ...options,
288
+ signal: context.signal,
289
+ ...(reportStats
290
+ ? {
291
+ onStats: (stats) => {
292
+ this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
293
+ },
294
+ }
295
+ : {}),
296
+ })));
241
297
  }
242
298
  case "queryCursorOpen": {
243
299
  const handleId = this.#claimHandleId(args[0]);
244
- const [sql, options] = args.slice(1);
300
+ const [sql, options, reportStats = false] = args.slice(1);
245
301
  try {
246
- const iterator = this.database.queryCursor(sql, options);
302
+ const iterator = this.database.queryCursor(sql, {
303
+ ...options,
304
+ ...(reportStats
305
+ ? {
306
+ onStats: (stats) => {
307
+ this.scope.postMessage(rpcEvent(handleId, "stats", stats));
308
+ },
309
+ }
310
+ : {}),
311
+ });
247
312
  this.#publishHandle(handleId, { type: "query-cursor", iterator });
248
313
  }
249
314
  catch (error) {
@@ -465,14 +530,14 @@ class DatabaseRpcServer {
465
530
  void opening.then(() => this.#openingHandlePromises.delete(opening), () => this.#openingHandlePromises.delete(opening));
466
531
  return opening;
467
532
  }
468
- async #callHandle(handleId, method, args) {
533
+ async #callHandle(handleId, method, args, context) {
469
534
  const handle = this.#handles.get(handleId);
470
535
  if (handle === undefined)
471
536
  throw new Error(`Unknown handle: ${handleId}`);
472
537
  if (handle.type === "write") {
473
538
  this.#beginWriteHandleCall(handle);
474
539
  try {
475
- return await this.#callWriteHandle(handleId, handle, method, args);
540
+ return await this.#callWriteHandle(handleId, handle, method, args, context);
476
541
  }
477
542
  finally {
478
543
  this.#endWriteHandleCall(handleId, handle);
@@ -523,10 +588,20 @@ class DatabaseRpcServer {
523
588
  }
524
589
  }
525
590
  }
526
- async #callWriteHandle(handleId, handle, method, args) {
591
+ async #callWriteHandle(handleId, handle, method, args, context) {
527
592
  if (method === "query") {
528
- const [sql, options] = args;
529
- return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, options)));
593
+ const [sql, options, reportStats = false] = args;
594
+ return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, {
595
+ ...options,
596
+ signal: context.signal,
597
+ ...(reportStats
598
+ ? {
599
+ onStats: (stats) => {
600
+ this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
601
+ },
602
+ }
603
+ : {}),
604
+ })));
530
605
  }
531
606
  if (method === "execute") {
532
607
  const [sql, params] = args;
@@ -7,10 +7,11 @@
7
7
  * Exposing them here lets an external builder produce plans the engine treats as indistinguishable
8
8
  * from parsed SQL.
9
9
  *
10
- * This module re-exports rather than owns: the plan types still live beside the parser that
11
- * produces them. Splitting them into a leaf module of their own is the next step, and it will not
12
- * change this entry point.
10
+ * The recursive logical model lives in the dependency-light `model` leaf; executable assembly
11
+ * and validation stay with the compiler. This entry point keeps that distinction invisible to
12
+ * consumers.
13
13
  */
14
- export { assembleSelectBlock, compoundSelectBlock, derivedTableSource, hasAggregate, splitCondition, validateLimit, validateOffset, type AggregateName, type CompiledQuery, type CompiledStatement, type Expression, type JoinPlan, type Predicate, type PredicateOperator, type QueryResult, type QueryRow, type QueryValue, type SelectItem, type SetOperator, type TableSource, type WindowFunctionName, } from "../engine/query.js";
14
+ export { assembleSelectBlock, compoundSelectBlock, derivedTableSource, hasAggregate, splitCondition, validateLimit, validateOffset, type CompiledStatement, } from "../engine/query.js";
15
+ export type { AggregateName, CompiledQuery, Expression, JoinPlan, Predicate, PredicateOperator, QueryResult, QueryRow, QueryValue, SelectItem, SetOperator, TableSource, WindowFunctionName, } from "./model.js";
15
16
  export { optimizePlan, renderPlan } from "../engine/optimizer.js";
16
17
  export { validateFtsQuery } from "../engine/fts.js";
@@ -7,9 +7,9 @@
7
7
  * Exposing them here lets an external builder produce plans the engine treats as indistinguishable
8
8
  * from parsed SQL.
9
9
  *
10
- * This module re-exports rather than owns: the plan types still live beside the parser that
11
- * produces them. Splitting them into a leaf module of their own is the next step, and it will not
12
- * change this entry point.
10
+ * The recursive logical model lives in the dependency-light `model` leaf; executable assembly
11
+ * and validation stay with the compiler. This entry point keeps that distinction invisible to
12
+ * consumers.
13
13
  */
14
14
  export { assembleSelectBlock, compoundSelectBlock, derivedTableSource, hasAggregate, splitCondition, validateLimit, validateOffset, } from "../engine/query.js";
15
15
  export { optimizePlan, renderPlan } from "../engine/optimizer.js";
@@ -0,0 +1,218 @@
1
+ /**
2
+ * The dependency-light logical plan shared by the SQL compiler, optimizer, vector executor, and
3
+ * external typed-query builders. Keep executable parser/engine code out of this module: importing
4
+ * these types must never create a runtime dependency edge.
5
+ */
6
+ import type { SqlDomain } from "../storage/types.js";
7
+ export type QueryValue = boolean | number | string | Date | null;
8
+ export type QueryRow = Record<string, QueryValue>;
9
+ export interface QueryResult {
10
+ columns: string[];
11
+ /** Logical SQL domain for each output column, positionally aligned with `columns`. */
12
+ columnDomains: Array<SqlDomain | null>;
13
+ rows: QueryRow[];
14
+ }
15
+ export type BinaryOperator = "+" | "-" | "*" | "/" | "%" | "||";
16
+ export type ComparisonOperator = "=" | "!=" | "<>" | ">" | ">=" | "<" | "<=";
17
+ export type AggregateName = "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "JSON_ARRAYAGG" | "STRING_AGG";
18
+ export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD" | "UPPER" | "LOWER" | "LENGTH" | "ABS" | "TRIM" | "LTRIM" | "RTRIM" | "SUBSTR" | "REPLACE" | "INSTR" | "NULLIF" | "GREATEST" | "LEAST" | "FLOOR" | "CEIL" | "MOD" | "POWER" | "SQRT" | "EXTRACT" | "CAST" | "OCTET_LENGTH" | "LPAD" | "RPAD" | "OVERLAY" | "CURRENT_DATE" | "CURRENT_TIMESTAMP" | "LOCALTIME" | "GROUPING" | "JSON_VALUE" | "JSON_QUERY" | "JSON_EXISTS" | "JSON_OBJECT" | "JSON_ARRAY" | "IS_JSON" | "ARRAY"
19
+ /** Optimizer-only, prefix-free equality key for hashable multi-column decorrelation. */
20
+ | "MINNOW_TUPLE_KEY"
21
+ /** Parser-produced wrapper carrying one explicit collation through ordering/comparison. */
22
+ | "MINNOW_COLLATE" | "NEXTVAL" | "CURRVAL" | "RANDOM" | "GEN_RANDOM_UUID";
23
+ /** Exact BM25 corpus statistics attached to a cloned scoring node before execution. */
24
+ export interface FtsStats {
25
+ /** Every row of the corpus, including all-null documents. */
26
+ docCount: number;
27
+ /** Total tokens across all documents. */
28
+ totalTokens: number;
29
+ /** Documents containing each query term, aligned with the query's term order. */
30
+ dfByTerm: number[];
31
+ }
32
+ export type Expression = {
33
+ kind: "literal";
34
+ value: QueryValue;
35
+ internalSqlValue?: true;
36
+ sqlDomain?: SqlDomain;
37
+ }
38
+ /** A `?` or `$n` placeholder; `index` is 0-based. Replaced by a literal at bind time. */
39
+ | {
40
+ kind: "parameter";
41
+ index: number;
42
+ } | {
43
+ kind: "column";
44
+ reference: string;
45
+ }
46
+ /** `*`, or `alias.*` when `table` is set. */
47
+ | {
48
+ kind: "wildcard";
49
+ table?: string;
50
+ } | {
51
+ kind: "binary";
52
+ operator: BinaryOperator;
53
+ left: Expression;
54
+ right: Expression;
55
+ } | {
56
+ kind: "call";
57
+ name: AggregateName | ScalarFunctionName;
58
+ arguments: Expression[];
59
+ distinct?: boolean;
60
+ aggregateOrderBy?: Array<{
61
+ expression: Expression;
62
+ direction: "asc" | "desc";
63
+ nulls?: "first" | "last";
64
+ }>;
65
+ } | {
66
+ kind: "list";
67
+ items: Expression[];
68
+ } | {
69
+ kind: "subquery";
70
+ block: CompiledQuery;
71
+ } | {
72
+ kind: "condition";
73
+ operator: PredicateOperator;
74
+ left: Expression;
75
+ right: Expression;
76
+ escape?: string;
77
+ } | {
78
+ kind: "logical";
79
+ operator: "and" | "or";
80
+ left: Expression;
81
+ right: Expression;
82
+ } | {
83
+ kind: "not";
84
+ operand: Expression;
85
+ } | {
86
+ kind: "exists";
87
+ block: CompiledQuery;
88
+ negated: boolean;
89
+ } | {
90
+ kind: "case";
91
+ branches: Array<{
92
+ when: Expression;
93
+ then: Expression;
94
+ }>;
95
+ otherwise?: Expression;
96
+ } | {
97
+ kind: "window";
98
+ name: WindowFunctionName;
99
+ partitionBy: Expression[];
100
+ orderBy: Array<{
101
+ expression: Expression;
102
+ direction: "asc" | "desc";
103
+ nulls?: "first" | "last";
104
+ }>;
105
+ argument?: Expression;
106
+ offset?: number;
107
+ fallback?: QueryValue;
108
+ frame?: WindowFrame;
109
+ } | {
110
+ kind: "fts";
111
+ op: "match" | "bm25";
112
+ /** Column references forming the document, or `*` for all searchable scan columns. */
113
+ columns: Expression[] | "*";
114
+ query: string;
115
+ queryParameter?: number;
116
+ stats?: FtsStats;
117
+ };
118
+ export type WindowFunctionName = "ROW_NUMBER" | "RANK" | "DENSE_RANK" | "PERCENT_RANK" | "CUME_DIST" | "NTILE" | "LAG" | "LEAD" | "FIRST_VALUE" | "LAST_VALUE" | "NTH_VALUE" | AggregateName;
119
+ export interface WindowFrameBound {
120
+ kind: "unbounded-preceding" | "preceding" | "current-row" | "following" | "unbounded-following";
121
+ offset?: number;
122
+ }
123
+ export type WindowFrameExclusion = "no-others" | "current-row" | "group" | "ties";
124
+ export interface WindowFrame {
125
+ unit: "rows" | "range" | "groups";
126
+ start: WindowFrameBound;
127
+ end: WindowFrameBound;
128
+ exclude?: WindowFrameExclusion;
129
+ }
130
+ export interface WindowSpec {
131
+ alias: string;
132
+ name: WindowFunctionName;
133
+ partitionAliases: string[];
134
+ orderAliases: Array<{
135
+ alias: string;
136
+ direction: "asc" | "desc";
137
+ nulls?: "first" | "last";
138
+ }>;
139
+ argumentAlias?: string;
140
+ offset?: number;
141
+ fallback?: QueryValue;
142
+ frame?: WindowFrame;
143
+ }
144
+ export interface SelectItem {
145
+ expression: Expression;
146
+ alias: string;
147
+ }
148
+ export interface TableSource {
149
+ table: string;
150
+ alias: string;
151
+ derived?: CompiledQuery;
152
+ union?: {
153
+ blocks: CompiledQuery[];
154
+ ops: SetOperator[];
155
+ };
156
+ recursive?: RecursiveCte;
157
+ windowed?: {
158
+ block: CompiledQuery;
159
+ windows: WindowSpec[];
160
+ };
161
+ columnAliases?: string[];
162
+ lateral?: true;
163
+ }
164
+ export interface JoinPlan extends TableSource {
165
+ kind: "inner" | "left" | "semi" | "anti";
166
+ left: Expression;
167
+ right: Expression;
168
+ on?: Expression;
169
+ full?: boolean;
170
+ natural?: boolean;
171
+ }
172
+ export type SetOperator = "union" | "union all" | "intersect" | "intersect all" | "except" | "except all";
173
+ export interface RecursiveCte {
174
+ reference: string;
175
+ base: CompiledQuery;
176
+ step: CompiledQuery;
177
+ all: boolean;
178
+ }
179
+ export type PredicateOperator = ComparisonOperator | `${ComparisonOperator} ANY` | `${ComparisonOperator} ALL` | "IN" | "NOT IN" | "IS NULL" | "IS NOT NULL" | "LIKE" | "NOT LIKE" | "ILIKE" | "NOT ILIKE" | "SIMILAR TO" | "NOT SIMILAR TO" | "IS DISTINCT FROM" | "IS NOT DISTINCT FROM" | "IS TRUE";
180
+ export interface Predicate {
181
+ left: Expression;
182
+ operator: PredicateOperator;
183
+ right: Expression;
184
+ escape?: string;
185
+ }
186
+ export interface CompiledQuery {
187
+ sql: string;
188
+ base: TableSource;
189
+ joins: JoinPlan[];
190
+ select: SelectItem[];
191
+ predicates: Predicate[];
192
+ groupBy: Expression[];
193
+ having: Predicate[];
194
+ orderBy: Array<{
195
+ expression: Expression;
196
+ direction: "asc" | "desc";
197
+ nulls?: "first" | "last";
198
+ }>;
199
+ limit?: number;
200
+ offset?: number;
201
+ distinctWildcard?: boolean;
202
+ limitParameter?: number;
203
+ offsetParameter?: number;
204
+ limitWithTies?: boolean;
205
+ parameterCount?: number;
206
+ usesStatementDatetime?: boolean;
207
+ usesSequenceCalls?: boolean;
208
+ usesVolatileFunctions?: boolean;
209
+ }
210
+ /** ORDER BY / LIMIT / OFFSET tail of a select or set operation. */
211
+ export interface SelectTail {
212
+ orderBy: CompiledQuery["orderBy"];
213
+ limit?: number;
214
+ offset?: number;
215
+ limitParameter?: number;
216
+ offsetParameter?: number;
217
+ limitWithTies?: boolean;
218
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -136,6 +136,11 @@ export type ColumnDefault = {
136
136
  } | {
137
137
  kind: "autoincrement";
138
138
  };
139
+ /** A stored generated-column expression, evaluated from the row on every insert and update. */
140
+ export interface ColumnGenerated {
141
+ readonly kind: "stored";
142
+ readonly sql: string;
143
+ }
139
144
  export interface TableColumnRecord {
140
145
  id: string;
141
146
  name: string;
@@ -152,6 +157,8 @@ export interface TableColumnRecord {
152
157
  nullable: boolean;
153
158
  /** Fills omitted or SQL `DEFAULT` slots at insert time; explicit NULL is never replaced. */
154
159
  defaultValue?: ColumnDefault;
160
+ /** Recomputed from sibling columns on every insert and update; callers cannot assign it. */
161
+ generatedValue?: ColumnGenerated;
155
162
  /**
156
163
  * What rows written before this column existed read as, instead of NULL.
157
164
  *
@@ -141,6 +141,22 @@ export function validateTableColumns(columns) {
141
141
  ...(column.enumValues === undefined ? {} : { enumValues: column.enumValues }),
142
142
  }, column.defaultValue);
143
143
  }
144
+ if (column.generatedValue !== undefined) {
145
+ const generated = column.generatedValue;
146
+ if (typeof generated !== "object" ||
147
+ generated === null ||
148
+ !("kind" in generated) ||
149
+ generated.kind !== "stored" ||
150
+ !("sql" in generated) ||
151
+ typeof generated.sql !== "string" ||
152
+ generated.sql.length === 0 ||
153
+ generated.sql.trim() !== generated.sql) {
154
+ throw new TypeError(`Generated SQL must be a trimmed non-empty expression: ${column.name}`);
155
+ }
156
+ if (column.defaultValue !== undefined || column.backfill !== undefined || column.hidden) {
157
+ throw new TypeError(`Generated columns cannot have defaults, backfills, or hidden metadata: ${column.name}`);
158
+ }
159
+ }
144
160
  if (column.backfill !== undefined) {
145
161
  const value = column.backfill;
146
162
  const validType = column.type === "datetime"
@@ -195,9 +195,11 @@ export declare class DatabaseTransaction {
195
195
  setUniqueKeyChanges(changes: UniqueKeyChanges): void;
196
196
  /**
197
197
  * Attaches one batch's full-text deltas; applied atomically with the publish. A second
198
- * batch for the same table merges per column — postings re-sorted by term (each batch's
199
- * reserved row ids are strictly above the last, so rowIds stay ascending within a term)
200
- * and token totals summed — so a scope can insert into one FTS table any number of times.
198
+ * batch for the same table merges per column — postings stay sorted by term and row ID,
199
+ * duplicate row locators collapse to their greatest term frequency, and token totals are
200
+ * summed — so a scope can mutate one indexed table any number of times. Secondary indexes
201
+ * use stable hashed key locators rather than reserved row IDs, so operation order does not
202
+ * imply locator order.
201
203
  */
202
204
  setFtsChanges(changes: FtsChanges): void;
203
205
  /**
@@ -712,9 +712,11 @@ export class DatabaseTransaction {
712
712
  }
713
713
  /**
714
714
  * Attaches one batch's full-text deltas; applied atomically with the publish. A second
715
- * batch for the same table merges per column — postings re-sorted by term (each batch's
716
- * reserved row ids are strictly above the last, so rowIds stay ascending within a term)
717
- * and token totals summed — so a scope can insert into one FTS table any number of times.
715
+ * batch for the same table merges per column — postings stay sorted by term and row ID,
716
+ * duplicate row locators collapse to their greatest term frequency, and token totals are
717
+ * summed — so a scope can mutate one indexed table any number of times. Secondary indexes
718
+ * use stable hashed key locators rather than reserved row IDs, so operation order does not
719
+ * imply locator order.
718
720
  */
719
721
  setFtsChanges(changes) {
720
722
  this.#assertActive();
@@ -746,6 +748,7 @@ export class DatabaseTransaction {
746
748
  });
747
749
  continue;
748
750
  }
751
+ let duplicateTokens = 0;
749
752
  for (const posting of column.postings) {
750
753
  const held = present.postings.get(posting.term);
751
754
  if (held === undefined) {
@@ -756,13 +759,18 @@ export class DatabaseTransaction {
756
759
  });
757
760
  }
758
761
  else {
759
- for (const rowId of posting.rowIds)
760
- held.rowIds.push(rowId);
761
- for (const tf of posting.tf)
762
- held.tf.push(tf);
762
+ const merged = mergeFtsPostingRows(held, posting);
763
+ duplicateTokens = safeWholeNumberSum([
764
+ duplicateTokens,
765
+ postingTokenCount(held) + postingTokenCount(posting) - postingTokenCount(merged),
766
+ ], "Duplicate posting token count");
767
+ present.postings.set(posting.term, merged);
763
768
  }
764
769
  }
765
- present.totalTokens = tokenTotals.get(column.columnId) ?? present.totalTokens;
770
+ const totalTokens = tokenTotals.get(column.columnId) ?? present.totalTokens;
771
+ if (duplicateTokens > totalTokens)
772
+ throw new Error("Posting token merge is inconsistent");
773
+ present.totalTokens = totalTokens - duplicateTokens;
766
774
  }
767
775
  }
768
776
  #materializedFtsChanges() {
@@ -1615,6 +1623,48 @@ function safeWholeNumberSum(values, label) {
1615
1623
  }
1616
1624
  return total;
1617
1625
  }
1626
+ /** Linear merge of two canonical postings for one term. */
1627
+ function mergeFtsPostingRows(left, right) {
1628
+ if (left.term !== right.term)
1629
+ throw new TypeError("Posting terms differ during transaction merge");
1630
+ const rowIds = [];
1631
+ const tf = [];
1632
+ let leftIndex = 0;
1633
+ let rightIndex = 0;
1634
+ const push = (rowId, frequency) => {
1635
+ const previous = rowIds[rowIds.length - 1];
1636
+ if (previous === rowId) {
1637
+ tf[tf.length - 1] = Math.max(tf[tf.length - 1] ?? 1, frequency);
1638
+ return;
1639
+ }
1640
+ rowIds.push(rowId);
1641
+ tf.push(frequency);
1642
+ };
1643
+ while (leftIndex < left.rowIds.length && rightIndex < right.rowIds.length) {
1644
+ const leftRowId = left.rowIds[leftIndex] ?? 0n;
1645
+ const rightRowId = right.rowIds[rightIndex] ?? 0n;
1646
+ if (leftRowId <= rightRowId) {
1647
+ push(leftRowId, left.tf[leftIndex] ?? 1);
1648
+ leftIndex += 1;
1649
+ }
1650
+ if (rightRowId <= leftRowId) {
1651
+ push(rightRowId, right.tf[rightIndex] ?? 1);
1652
+ rightIndex += 1;
1653
+ }
1654
+ }
1655
+ while (leftIndex < left.rowIds.length) {
1656
+ push(left.rowIds[leftIndex] ?? 0n, left.tf[leftIndex] ?? 1);
1657
+ leftIndex += 1;
1658
+ }
1659
+ while (rightIndex < right.rowIds.length) {
1660
+ push(right.rowIds[rightIndex] ?? 0n, right.tf[rightIndex] ?? 1);
1661
+ rightIndex += 1;
1662
+ }
1663
+ return { term: left.term, rowIds, tf };
1664
+ }
1665
+ function postingTokenCount(posting) {
1666
+ return safeWholeNumberSum(posting.tf, "Posting token count");
1667
+ }
1618
1668
  function requiredCompactionJobId(id) {
1619
1669
  if (id === null)
1620
1670
  throw new Error("Compaction source blocks require a compaction job");
@@ -1,4 +1,4 @@
1
- export declare const protocolVersion: 2;
1
+ export declare const protocolVersion: 3;
2
2
  /** Outstanding request/response pairs retained by either side of one database RPC connection. */
3
3
  export declare const MAX_DATABASE_RPC_IN_FLIGHT = 256;
4
4
  export type WorkerOperation = "benchmark" | "cancelBenchmark" | "memorySample" | "datasetList" | "datasetCreate" | "datasetDelete" | "runQuery" | "suiteReference" | "suiteWrite" | "suiteFeatureMatrix" | "suiteLive";
@@ -46,6 +46,11 @@ export type RpcRequest = {
46
46
  handleId: string | null;
47
47
  method: string;
48
48
  args: unknown[];
49
+ } | {
50
+ version: typeof protocolVersion;
51
+ /** The request whose work should stop. Cancellation has no response frame of its own. */
52
+ requestId: string;
53
+ kind: "rpc-cancel";
49
54
  };
50
55
  export type RpcResponse = {
51
56
  version: typeof protocolVersion;