@minnowdb/core 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/engine/cancellation.d.ts +2 -0
- package/dist/engine/cancellation.js +4 -0
- package/dist/engine/catalog.d.ts +3 -1
- package/dist/engine/catalog.js +1 -0
- package/dist/engine/client.d.ts +32 -4
- package/dist/engine/client.js +82 -15
- package/dist/engine/database.d.ts +23 -14
- package/dist/engine/database.js +528 -79
- package/dist/engine/defaults.js +11 -0
- package/dist/engine/errors.d.ts +13 -0
- package/dist/engine/errors.js +22 -0
- package/dist/engine/fts.d.ts +2 -15
- package/dist/engine/live.d.ts +1 -7
- package/dist/engine/live.js +2 -12
- package/dist/engine/optimizer.d.ts +7 -0
- package/dist/engine/optimizer.js +1349 -76
- package/dist/engine/query.d.ts +11 -278
- package/dist/engine/query.js +178 -49
- package/dist/engine/schema-wire.d.ts +7 -1
- package/dist/engine/schema-wire.js +4 -0
- package/dist/engine/schema.d.ts +67 -33
- package/dist/engine/schema.js +138 -7
- package/dist/engine/sql-domains.d.ts +8 -0
- package/dist/engine/sql-domains.js +25 -0
- package/dist/engine/sql-json.js +22 -3
- package/dist/engine/vector.d.ts +2 -2
- package/dist/engine/vector.js +369 -43
- package/dist/engine/worker-host.js +119 -44
- package/dist/plan/index.d.ts +5 -4
- package/dist/plan/index.js +3 -3
- package/dist/plan/model.d.ts +224 -0
- package/dist/plan/model.js +1 -0
- package/dist/storage/types.d.ts +7 -0
- package/dist/storage/types.js +16 -0
- package/dist/transactions/index.d.ts +5 -3
- package/dist/transactions/index.js +58 -8
- package/dist/worker-protocol/index.d.ts +6 -1
- package/dist/worker-protocol/index.js +5 -2
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +89 -21
|
@@ -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,
|
|
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,
|
|
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,
|
|
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;
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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";
|
package/dist/plan/index.js
CHANGED
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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,224 @@
|
|
|
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
|
+
/** Optimizer-only aggregate that enforces scalar-subquery cardinality. */
|
|
19
|
+
| "MINNOW_SINGLE_VALUE";
|
|
20
|
+
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"
|
|
21
|
+
/** Optimizer-only, prefix-free equality key for hashable multi-column decorrelation. */
|
|
22
|
+
| "MINNOW_TUPLE_KEY"
|
|
23
|
+
/** Parser-produced wrapper carrying one explicit collation through ordering/comparison. */
|
|
24
|
+
| "MINNOW_COLLATE" | "NEXTVAL" | "CURRVAL" | "RANDOM" | "GEN_RANDOM_UUID";
|
|
25
|
+
/** Exact BM25 corpus statistics attached to a cloned scoring node before execution. */
|
|
26
|
+
export interface FtsStats {
|
|
27
|
+
/** Every row of the corpus, including all-null documents. */
|
|
28
|
+
docCount: number;
|
|
29
|
+
/** Total tokens across all documents. */
|
|
30
|
+
totalTokens: number;
|
|
31
|
+
/** Documents containing each query term, aligned with the query's term order. */
|
|
32
|
+
dfByTerm: number[];
|
|
33
|
+
}
|
|
34
|
+
export type Expression = {
|
|
35
|
+
kind: "literal";
|
|
36
|
+
value: QueryValue;
|
|
37
|
+
internalSqlValue?: true;
|
|
38
|
+
sqlDomain?: SqlDomain;
|
|
39
|
+
}
|
|
40
|
+
/** A `?` or `$n` placeholder; `index` is 0-based. Replaced by a literal at bind time. */
|
|
41
|
+
| {
|
|
42
|
+
kind: "parameter";
|
|
43
|
+
index: number;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "column";
|
|
46
|
+
reference: string;
|
|
47
|
+
}
|
|
48
|
+
/** `*`, or `alias.*` when `table` is set. */
|
|
49
|
+
| {
|
|
50
|
+
kind: "wildcard";
|
|
51
|
+
table?: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: "binary";
|
|
54
|
+
operator: BinaryOperator;
|
|
55
|
+
left: Expression;
|
|
56
|
+
right: Expression;
|
|
57
|
+
} | {
|
|
58
|
+
kind: "call";
|
|
59
|
+
name: AggregateName | ScalarFunctionName;
|
|
60
|
+
arguments: Expression[];
|
|
61
|
+
distinct?: boolean;
|
|
62
|
+
aggregateOrderBy?: Array<{
|
|
63
|
+
expression: Expression;
|
|
64
|
+
direction: "asc" | "desc";
|
|
65
|
+
nulls?: "first" | "last";
|
|
66
|
+
}>;
|
|
67
|
+
} | {
|
|
68
|
+
kind: "list";
|
|
69
|
+
items: Expression[];
|
|
70
|
+
} | {
|
|
71
|
+
kind: "subquery";
|
|
72
|
+
block: CompiledQuery;
|
|
73
|
+
} | {
|
|
74
|
+
kind: "condition";
|
|
75
|
+
operator: PredicateOperator;
|
|
76
|
+
left: Expression;
|
|
77
|
+
right: Expression;
|
|
78
|
+
escape?: string;
|
|
79
|
+
} | {
|
|
80
|
+
kind: "logical";
|
|
81
|
+
operator: "and" | "or";
|
|
82
|
+
left: Expression;
|
|
83
|
+
right: Expression;
|
|
84
|
+
} | {
|
|
85
|
+
kind: "not";
|
|
86
|
+
operand: Expression;
|
|
87
|
+
} | {
|
|
88
|
+
kind: "exists";
|
|
89
|
+
block: CompiledQuery;
|
|
90
|
+
negated: boolean;
|
|
91
|
+
} | {
|
|
92
|
+
kind: "case";
|
|
93
|
+
branches: Array<{
|
|
94
|
+
when: Expression;
|
|
95
|
+
then: Expression;
|
|
96
|
+
}>;
|
|
97
|
+
otherwise?: Expression;
|
|
98
|
+
} | {
|
|
99
|
+
kind: "window";
|
|
100
|
+
name: WindowFunctionName;
|
|
101
|
+
partitionBy: Expression[];
|
|
102
|
+
orderBy: Array<{
|
|
103
|
+
expression: Expression;
|
|
104
|
+
direction: "asc" | "desc";
|
|
105
|
+
nulls?: "first" | "last";
|
|
106
|
+
}>;
|
|
107
|
+
argument?: Expression;
|
|
108
|
+
offset?: number;
|
|
109
|
+
fallback?: QueryValue;
|
|
110
|
+
frame?: WindowFrame;
|
|
111
|
+
} | {
|
|
112
|
+
kind: "fts";
|
|
113
|
+
op: "match" | "bm25";
|
|
114
|
+
/** Column references forming the document, or `*` for all searchable scan columns. */
|
|
115
|
+
columns: Expression[] | "*";
|
|
116
|
+
query: string;
|
|
117
|
+
queryParameter?: number;
|
|
118
|
+
stats?: FtsStats;
|
|
119
|
+
};
|
|
120
|
+
export type WindowFunctionName = "ROW_NUMBER" | "RANK" | "DENSE_RANK" | "PERCENT_RANK" | "CUME_DIST" | "NTILE" | "LAG" | "LEAD" | "FIRST_VALUE" | "LAST_VALUE" | "NTH_VALUE" | AggregateName;
|
|
121
|
+
export interface WindowFrameBound {
|
|
122
|
+
kind: "unbounded-preceding" | "preceding" | "current-row" | "following" | "unbounded-following";
|
|
123
|
+
offset?: number;
|
|
124
|
+
}
|
|
125
|
+
export type WindowFrameExclusion = "no-others" | "current-row" | "group" | "ties";
|
|
126
|
+
export interface WindowFrame {
|
|
127
|
+
unit: "rows" | "range" | "groups";
|
|
128
|
+
start: WindowFrameBound;
|
|
129
|
+
end: WindowFrameBound;
|
|
130
|
+
exclude?: WindowFrameExclusion;
|
|
131
|
+
}
|
|
132
|
+
export interface WindowSpec {
|
|
133
|
+
alias: string;
|
|
134
|
+
name: WindowFunctionName;
|
|
135
|
+
partitionAliases: string[];
|
|
136
|
+
orderAliases: Array<{
|
|
137
|
+
alias: string;
|
|
138
|
+
direction: "asc" | "desc";
|
|
139
|
+
nulls?: "first" | "last";
|
|
140
|
+
}>;
|
|
141
|
+
argumentAlias?: string;
|
|
142
|
+
offset?: number;
|
|
143
|
+
fallback?: QueryValue;
|
|
144
|
+
frame?: WindowFrame;
|
|
145
|
+
}
|
|
146
|
+
export interface SelectItem {
|
|
147
|
+
expression: Expression;
|
|
148
|
+
alias: string;
|
|
149
|
+
}
|
|
150
|
+
export interface TableSource {
|
|
151
|
+
table: string;
|
|
152
|
+
alias: string;
|
|
153
|
+
derived?: CompiledQuery;
|
|
154
|
+
union?: {
|
|
155
|
+
blocks: CompiledQuery[];
|
|
156
|
+
ops: SetOperator[];
|
|
157
|
+
};
|
|
158
|
+
recursive?: RecursiveCte;
|
|
159
|
+
windowed?: {
|
|
160
|
+
block: CompiledQuery;
|
|
161
|
+
windows: WindowSpec[];
|
|
162
|
+
};
|
|
163
|
+
columnAliases?: string[];
|
|
164
|
+
lateral?: true;
|
|
165
|
+
}
|
|
166
|
+
export interface JoinPlan extends TableSource {
|
|
167
|
+
kind: "inner" | "left" | "semi" | "anti";
|
|
168
|
+
left: Expression;
|
|
169
|
+
right: Expression;
|
|
170
|
+
on?: Expression;
|
|
171
|
+
full?: boolean;
|
|
172
|
+
natural?: boolean;
|
|
173
|
+
}
|
|
174
|
+
export type SetOperator = "union" | "union all" | "intersect" | "intersect all" | "except" | "except all";
|
|
175
|
+
export interface RecursiveCte {
|
|
176
|
+
reference: string;
|
|
177
|
+
base: CompiledQuery;
|
|
178
|
+
step: CompiledQuery;
|
|
179
|
+
all: boolean;
|
|
180
|
+
}
|
|
181
|
+
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";
|
|
182
|
+
export interface Predicate {
|
|
183
|
+
left: Expression;
|
|
184
|
+
operator: PredicateOperator;
|
|
185
|
+
right: Expression;
|
|
186
|
+
escape?: string;
|
|
187
|
+
}
|
|
188
|
+
export interface CompiledQuery {
|
|
189
|
+
sql: string;
|
|
190
|
+
base: TableSource;
|
|
191
|
+
joins: JoinPlan[];
|
|
192
|
+
select: SelectItem[];
|
|
193
|
+
predicates: Predicate[];
|
|
194
|
+
groupBy: Expression[];
|
|
195
|
+
having: Predicate[];
|
|
196
|
+
orderBy: Array<{
|
|
197
|
+
expression: Expression;
|
|
198
|
+
direction: "asc" | "desc";
|
|
199
|
+
nulls?: "first" | "last";
|
|
200
|
+
}>;
|
|
201
|
+
limit?: number;
|
|
202
|
+
offset?: number;
|
|
203
|
+
distinctWildcard?: boolean;
|
|
204
|
+
limitParameter?: number;
|
|
205
|
+
offsetParameter?: number;
|
|
206
|
+
/** Optimizer-relocated LIMIT placeholders that still need bind-time numeric/range validation. */
|
|
207
|
+
limitValidationParameters?: number[];
|
|
208
|
+
/** Optimizer-relocated OFFSET placeholders that still need bind-time numeric/range validation. */
|
|
209
|
+
offsetValidationParameters?: number[];
|
|
210
|
+
limitWithTies?: boolean;
|
|
211
|
+
parameterCount?: number;
|
|
212
|
+
usesStatementDatetime?: boolean;
|
|
213
|
+
usesSequenceCalls?: boolean;
|
|
214
|
+
usesVolatileFunctions?: boolean;
|
|
215
|
+
}
|
|
216
|
+
/** ORDER BY / LIMIT / OFFSET tail of a select or set operation. */
|
|
217
|
+
export interface SelectTail {
|
|
218
|
+
orderBy: CompiledQuery["orderBy"];
|
|
219
|
+
limit?: number;
|
|
220
|
+
offset?: number;
|
|
221
|
+
limitParameter?: number;
|
|
222
|
+
offsetParameter?: number;
|
|
223
|
+
limitWithTies?: boolean;
|
|
224
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/storage/types.js
CHANGED
|
@@ -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
|
|
199
|
-
*
|
|
200
|
-
*
|
|
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
|
|
716
|
-
*
|
|
717
|
-
*
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
held
|
|
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
|
-
|
|
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");
|