@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
package/dist/engine/schema.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ColumnDefault, type SqlDomain, type TableColumnRecord, type TableRecord } from "../storage/types.js";
|
|
2
2
|
import { type Catalog, type CatalogColumn, type CatalogTable } from "./catalog.js";
|
|
3
|
-
import { type QueryValue } from "./query.js";
|
|
3
|
+
import { type Expression, type QueryValue } from "./query.js";
|
|
4
4
|
/**
|
|
5
5
|
* Typed schema DSL and catalog migration planning. Column builders carry compile-time value and
|
|
6
6
|
* nullability types; `planMigration` diffs a schema against the live catalog into metadata-only
|
|
@@ -9,6 +9,14 @@ import { type QueryValue } from "./query.js";
|
|
|
9
9
|
* catalog-only, and older segments read the new column as NULL.
|
|
10
10
|
*/
|
|
11
11
|
export type SchemaColumnType = "boolean" | "number" | "string" | "datetime";
|
|
12
|
+
/** Compiles and validates one immutable, row-local generated-column expression. */
|
|
13
|
+
export declare function compileGeneratedColumnExpression(tableName: string, columnName: string, sql: string, columns: ReadonlyArray<{
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly generatedValue?: {
|
|
16
|
+
readonly kind: "stored";
|
|
17
|
+
readonly sql: string;
|
|
18
|
+
};
|
|
19
|
+
}>): Expression;
|
|
12
20
|
/** What a FOREIGN KEY does to child rows when the parent row is deleted (E141-04). */
|
|
13
21
|
export type ReferentialAction = "restrict" | "cascade" | "set null";
|
|
14
22
|
/** A declared relation. `onDelete` defaults to "restrict", matching SQL's own default. */
|
|
@@ -47,22 +55,29 @@ export declare function foreignKeyName(tableName: string, columnName: string): s
|
|
|
47
55
|
export type HasDefault<TValue> = TValue & {
|
|
48
56
|
readonly __minnowHasDefault?: true;
|
|
49
57
|
};
|
|
50
|
-
export interface ColumnBuilder<TValue extends SchemaValue, TNullable extends boolean, TUnique extends boolean = false, THasDefault extends boolean = false, TInput extends SchemaValue = TValue> {
|
|
58
|
+
export interface ColumnBuilder<TValue extends SchemaValue, TNullable extends boolean, TUnique extends boolean = false, THasDefault extends boolean = false, TInput extends SchemaValue = TValue, TDomain extends SqlDomain["kind"] | undefined = undefined, TGenerated extends boolean = false> {
|
|
51
59
|
readonly kind: "column";
|
|
52
60
|
readonly type: SchemaColumnType;
|
|
53
61
|
readonly isNullable: TNullable;
|
|
54
62
|
readonly isUnique: TUnique;
|
|
55
63
|
readonly hasDefault: THasDefault;
|
|
64
|
+
readonly isGenerated: TGenerated;
|
|
56
65
|
/** Type-only metadata consumed by adapters; optional so it emits no runtime payload. */
|
|
57
66
|
readonly "~types"?: {
|
|
58
67
|
readonly select: TValue;
|
|
59
68
|
readonly input: TInput;
|
|
69
|
+
readonly domain: TDomain;
|
|
70
|
+
readonly generated: TGenerated;
|
|
60
71
|
};
|
|
61
72
|
/** True for exact SQL integer columns; ordinary number columns use Float64 semantics. */
|
|
62
73
|
readonly integer: boolean;
|
|
63
74
|
/** Logical SQL semantics layered over the stable string storage encoding. */
|
|
64
75
|
readonly sqlDomain?: SqlDomain;
|
|
65
76
|
readonly defaultSpec?: ColumnDefault;
|
|
77
|
+
readonly generatedSpec?: {
|
|
78
|
+
readonly kind: "stored";
|
|
79
|
+
readonly sql: string;
|
|
80
|
+
};
|
|
66
81
|
/** Present on `column.enum()` builders: the closed set of values writes must draw from. */
|
|
67
82
|
readonly enumValues?: readonly string[];
|
|
68
83
|
/** What rows written before this column existed read as; see `.backfill()`. */
|
|
@@ -70,11 +85,11 @@ export interface ColumnBuilder<TValue extends SchemaValue, TNullable extends boo
|
|
|
70
85
|
readonly renamedFromName?: string;
|
|
71
86
|
readonly reference?: ColumnReferenceSpec;
|
|
72
87
|
/** Marks the column nullable; inserts may omit it and reads may return null. */
|
|
73
|
-
nullable(): ColumnBuilder<TValue, true, TUnique, THasDefault, TInput>;
|
|
88
|
+
nullable(): ColumnBuilder<TValue, true, TUnique, THasDefault, TInput, TDomain, TGenerated>;
|
|
74
89
|
/** Marks the table's unique key; exactly one non-nullable column may carry it. */
|
|
75
|
-
unique(this: ColumnBuilder<TValue, false, TUnique, THasDefault, TInput>): ColumnBuilder<TValue, TNullable, true, THasDefault, TInput>;
|
|
90
|
+
unique(this: ColumnBuilder<TValue, false, TUnique, THasDefault, TInput, TDomain, TGenerated>): ColumnBuilder<TValue, TNullable, true, THasDefault, TInput, TDomain, TGenerated>;
|
|
76
91
|
/** Declares this column as the rename target of an existing catalog column. */
|
|
77
|
-
renamedFrom(name: string): ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput>;
|
|
92
|
+
renamedFrom(name: string): ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput, TDomain, TGenerated>;
|
|
78
93
|
/**
|
|
79
94
|
* What rows written before this column existed read as, instead of NULL. Giving one is what
|
|
80
95
|
* makes adding a non-nullable column possible: no stored byte is rewritten, and reads
|
|
@@ -84,7 +99,7 @@ export interface ColumnBuilder<TValue extends SchemaValue, TNullable extends boo
|
|
|
84
99
|
* catalog — so it can derive a value (a timestamp, a version stamp) without readers ever
|
|
85
100
|
* disagreeing. It cannot derive from other columns; that would need a value per row.
|
|
86
101
|
*/
|
|
87
|
-
backfill(this: ColumnBuilder<TValue, false, TUnique, THasDefault, TInput>, value: TInput | (() => TInput)): ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput>;
|
|
102
|
+
backfill(this: ColumnBuilder<TValue, false, TUnique, THasDefault, TInput, TDomain, TGenerated>, value: TInput | (() => TInput)): ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput, TDomain, TGenerated>;
|
|
88
103
|
/**
|
|
89
104
|
* Declares a FOREIGN KEY onto another table's unique key. `migrate()` creates it as a real
|
|
90
105
|
* constraint, so a write naming a parent row that does not exist is rejected — the same
|
|
@@ -92,31 +107,33 @@ export interface ColumnBuilder<TValue extends SchemaValue, TNullable extends boo
|
|
|
92
107
|
*
|
|
93
108
|
* `onDelete` defaults to `"restrict"`. `"set null"` requires a nullable column.
|
|
94
109
|
*/
|
|
95
|
-
references(this: ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput>, table: string, column: string, options?: {
|
|
110
|
+
references(this: ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput, TDomain, TGenerated>, table: string, column: string, options?: {
|
|
96
111
|
onDelete?: Exclude<ReferentialAction, "set null">;
|
|
97
112
|
enforced?: true;
|
|
98
113
|
} | {
|
|
99
114
|
enforced: false;
|
|
100
115
|
onDelete?: never;
|
|
101
|
-
}): ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput>;
|
|
102
|
-
references(this: ColumnBuilder<TValue, true, TUnique, THasDefault, TInput>, table: string, column: string, options: {
|
|
116
|
+
}): ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput, TDomain, TGenerated>;
|
|
117
|
+
references(this: ColumnBuilder<TValue, true, TUnique, THasDefault, TInput, TDomain, TGenerated>, table: string, column: string, options: {
|
|
103
118
|
onDelete: "set null";
|
|
104
119
|
enforced?: true;
|
|
105
|
-
}): ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput>;
|
|
120
|
+
}): ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput, TDomain, TGenerated>;
|
|
106
121
|
/** Declares a literal SQL default. Omission or SQL `DEFAULT` invokes it; NULL does not. */
|
|
107
|
-
default(value: TInput): ColumnBuilder<TValue, TNullable, TUnique, true, TInput>;
|
|
122
|
+
default(value: TInput): ColumnBuilder<TValue, TNullable, TUnique, true, TInput, TDomain, TGenerated>;
|
|
108
123
|
/**
|
|
109
124
|
* Declares a variable-free SQL default expression, such as `CURRENT_TIMESTAMP`,
|
|
110
125
|
* `gen_random_uuid()`, or `nextval('orders_id_seq')`. The engine parses and type-checks it
|
|
111
126
|
* before migration and evaluates it once per omitted row.
|
|
112
127
|
*/
|
|
113
|
-
defaultSql(sql: string): ColumnBuilder<TValue, TNullable, TUnique, true, TInput>;
|
|
128
|
+
defaultSql(sql: string): ColumnBuilder<TValue, TNullable, TUnique, true, TInput, TDomain, TGenerated>;
|
|
129
|
+
/** Declares a stored expression over sibling columns. Callers never insert or update it. */
|
|
130
|
+
generatedSql(sql: string): ColumnBuilder<TValue, TNullable, TUnique, false, TInput, TDomain, true>;
|
|
114
131
|
/**
|
|
115
132
|
* Generates monotonically increasing integers for omitted or SQL `DEFAULT` slots from a persistent
|
|
116
133
|
* per-table counter that is atomic across tabs. Explicit values are allowed and bump the
|
|
117
134
|
* counter past their maximum. Number unique-key columns only.
|
|
118
135
|
*/
|
|
119
|
-
autoIncrement: TValue extends number ? (this: ColumnBuilder<TValue, false, true, THasDefault, TInput>) => ColumnBuilder<TValue, TNullable, TUnique, true, TInput> : never;
|
|
136
|
+
autoIncrement: TValue extends number ? (this: ColumnBuilder<TValue, false, true, THasDefault, TInput, TDomain, TGenerated>) => ColumnBuilder<TValue, TNullable, TUnique, true, TInput, TDomain, TGenerated> : never;
|
|
120
137
|
}
|
|
121
138
|
type SchemaValue = boolean | number | string | Date;
|
|
122
139
|
/**
|
|
@@ -130,9 +147,14 @@ interface AnyColumn {
|
|
|
130
147
|
readonly isNullable: boolean;
|
|
131
148
|
readonly isUnique: boolean;
|
|
132
149
|
readonly hasDefault: boolean;
|
|
150
|
+
readonly isGenerated: boolean;
|
|
133
151
|
readonly integer: boolean;
|
|
134
152
|
readonly sqlDomain?: SqlDomain;
|
|
135
153
|
readonly defaultSpec?: ColumnDefault;
|
|
154
|
+
readonly generatedSpec?: {
|
|
155
|
+
readonly kind: "stored";
|
|
156
|
+
readonly sql: string;
|
|
157
|
+
};
|
|
136
158
|
readonly enumValues?: readonly string[];
|
|
137
159
|
readonly backfillValue?: SchemaValue | (() => SchemaValue);
|
|
138
160
|
readonly renamedFromName?: string;
|
|
@@ -144,30 +166,30 @@ interface AnyColumn {
|
|
|
144
166
|
*/
|
|
145
167
|
export declare function columnWithDefaultSpec(base: Pick<AnyColumn, "type" | "isNullable" | "isUnique" | "renamedFromName" | "reference" | "enumValues"> & Partial<Pick<AnyColumn, "integer" | "sqlDomain">>, spec: ColumnDefault): AnyColumn;
|
|
146
168
|
/** Rebuilds a fluent column from structured-clone-safe metadata. */
|
|
147
|
-
export declare function columnFromState(state: Pick<AnyColumn, "type" | "isNullable" | "isUnique"> & Partial<Pick<AnyColumn, "integer" | "sqlDomain" | "renamedFromName" | "reference" | "defaultSpec" | "enumValues" | "backfillValue">>): AnyColumn;
|
|
169
|
+
export declare function columnFromState(state: Pick<AnyColumn, "type" | "isNullable" | "isUnique"> & Partial<Pick<AnyColumn, "integer" | "sqlDomain" | "renamedFromName" | "reference" | "defaultSpec" | "generatedSpec" | "enumValues" | "backfillValue">>): AnyColumn;
|
|
148
170
|
export declare const column: {
|
|
149
|
-
boolean: () => ColumnBuilder<boolean, false, false, false, boolean>;
|
|
150
|
-
number: () => ColumnBuilder<number, false, false, false, number>;
|
|
171
|
+
boolean: () => ColumnBuilder<boolean, false, false, false, boolean, undefined, false>;
|
|
172
|
+
number: () => ColumnBuilder<number, false, false, false, number, undefined, false>;
|
|
151
173
|
/** Exact safe-integer semantics, matching SQL INTEGER/SMALLINT/BIGINT. */
|
|
152
|
-
integer: () => ColumnBuilder<number, false, false, false, number>;
|
|
153
|
-
string: () => ColumnBuilder<string, false, false, false, string>;
|
|
154
|
-
datetime: () => ColumnBuilder<Date, false, false, false, Date>;
|
|
174
|
+
integer: () => ColumnBuilder<number, false, false, false, number, undefined, false>;
|
|
175
|
+
string: () => ColumnBuilder<string, false, false, false, string, undefined, false>;
|
|
176
|
+
datetime: () => ColumnBuilder<Date, false, false, false, Date, undefined, false>;
|
|
155
177
|
/** Exact decimal SQL NUMERIC. Selects return strings; writes accept strings or numbers. */
|
|
156
178
|
numeric: (options?: {
|
|
157
179
|
precision?: number;
|
|
158
180
|
scale?: number;
|
|
159
|
-
}) => ColumnBuilder<string, false, false, false, string | number>;
|
|
160
|
-
json: () => ColumnBuilder<string, false, false, false, string>;
|
|
161
|
-
jsonb: () => ColumnBuilder<string, false, false, false, string>;
|
|
162
|
-
uuid: () => ColumnBuilder<string, false, false, false, string>;
|
|
181
|
+
}) => ColumnBuilder<string, false, false, false, string | number, "numeric", false>;
|
|
182
|
+
json: () => ColumnBuilder<string, false, false, false, string, "json", false>;
|
|
183
|
+
jsonb: () => ColumnBuilder<string, false, false, false, string, "jsonb", false>;
|
|
184
|
+
uuid: () => ColumnBuilder<string, false, false, false, string, "uuid", false>;
|
|
163
185
|
/** A zoneless calendar date, represented publicly as canonical `YYYY-MM-DD` text. */
|
|
164
|
-
date: () => ColumnBuilder<string, false, false, false, string>;
|
|
165
|
-
time: () => ColumnBuilder<string, false, false, false, string>;
|
|
166
|
-
interval: () => ColumnBuilder<string, false, false, false, string>;
|
|
186
|
+
date: () => ColumnBuilder<string, false, false, false, string, "date", false>;
|
|
187
|
+
time: () => ColumnBuilder<string, false, false, false, string, "time", false>;
|
|
188
|
+
interval: () => ColumnBuilder<string, false, false, false, string, "interval", false>;
|
|
167
189
|
/** JSON array text at the JavaScript boundary, with the SQL element type retained in metadata. */
|
|
168
|
-
array: (element: string) => ColumnBuilder<string, false, false, false, string>;
|
|
190
|
+
array: (element: string) => ColumnBuilder<string, false, false, false, string, "array", false>;
|
|
169
191
|
/** A named SQL enum domain, distinct from the lightweight `column.enum()` restriction. */
|
|
170
|
-
sqlEnum: <const TValues extends readonly [string, ...string[]]>(name: string, values: TValues) => ColumnBuilder<TValues[number], false>;
|
|
192
|
+
sqlEnum: <const TValues extends readonly [string, ...string[]]>(name: string, values: TValues) => ColumnBuilder<TValues[number], false, false, false, TValues[number], "enum">;
|
|
171
193
|
/**
|
|
172
194
|
* A string column restricted to a closed set of values, typed as their literal union:
|
|
173
195
|
*
|
|
@@ -310,11 +332,15 @@ type NullableKeys<TTable extends AnyTable> = {
|
|
|
310
332
|
type DefaultKeys<TTable extends AnyTable> = {
|
|
311
333
|
[K in keyof TTable["columns"]]: TTable["columns"][K]["hasDefault"] extends true ? K : never;
|
|
312
334
|
}[keyof TTable["columns"]];
|
|
313
|
-
type
|
|
335
|
+
type GeneratedKeys<TTable extends AnyTable> = {
|
|
336
|
+
[K in keyof TTable["columns"]]: TTable["columns"][K]["isGenerated"] extends true ? K : never;
|
|
337
|
+
}[keyof TTable["columns"]];
|
|
338
|
+
type OptionalInsertKeys<TTable extends AnyTable> = Exclude<NullableKeys<TTable> | DefaultKeys<TTable>, GeneratedKeys<TTable>>;
|
|
339
|
+
type RequiredInsertKeys<TTable extends AnyTable> = Exclude<keyof TTable["columns"], OptionalInsertKeys<TTable> | GeneratedKeys<TTable>>;
|
|
314
340
|
/** Insert rows require every non-nullable column and may omit nullable or default-bearing ones. */
|
|
315
|
-
export type InferInsertRow<TTable extends AnyTable> =
|
|
316
|
-
[K in
|
|
317
|
-
}
|
|
341
|
+
export type InferInsertRow<TTable extends AnyTable> = {
|
|
342
|
+
[K in RequiredInsertKeys<TTable>]: ColumnInputValue<TTable["columns"][K]>;
|
|
343
|
+
} & {
|
|
318
344
|
[K in OptionalInsertKeys<TTable>]?: ColumnInputValue<TTable["columns"][K]>;
|
|
319
345
|
};
|
|
320
346
|
/** Scalar `.unique()` keys plus columns named by a table-level primary key. */
|
|
@@ -327,7 +353,7 @@ export type PrimaryKeyKeys<TTable extends AnyTable> = {
|
|
|
327
353
|
* stays assignable under `exactOptionalPropertyTypes`.
|
|
328
354
|
*/
|
|
329
355
|
export type InferUpdateChanges<TTable extends AnyTable> = {
|
|
330
|
-
[K in keyof TTable["columns"] as K extends PrimaryKeyKeys<TTable> ? never : K]?: ColumnInputValue<TTable["columns"][K]> | undefined;
|
|
356
|
+
[K in keyof TTable["columns"] as K extends PrimaryKeyKeys<TTable> | GeneratedKeys<TTable> ? never : K]?: ColumnInputValue<TTable["columns"][K]> | undefined;
|
|
331
357
|
};
|
|
332
358
|
export type MigrationStep = {
|
|
333
359
|
kind: "create-table";
|
|
@@ -376,6 +402,14 @@ export type MigrationStep = {
|
|
|
376
402
|
tableName: string;
|
|
377
403
|
columnName: string;
|
|
378
404
|
defaultValue: ColumnDefault | null;
|
|
405
|
+
} | {
|
|
406
|
+
kind: "alter-generated";
|
|
407
|
+
tableName: string;
|
|
408
|
+
columnName: string;
|
|
409
|
+
generatedValue: {
|
|
410
|
+
readonly kind: "stored";
|
|
411
|
+
readonly sql: string;
|
|
412
|
+
} | null;
|
|
379
413
|
}
|
|
380
414
|
/** Informational relationships are catalog-only and may change without scanning stored rows. */
|
|
381
415
|
| {
|
package/dist/engine/schema.js
CHANGED
|
@@ -1,7 +1,52 @@
|
|
|
1
1
|
import { copyDate, dateMilliseconds } from "../date-value.js";
|
|
2
2
|
import { validateColumnDefault, validateEnumValues, validateSqlDomain, } from "../storage/types.js";
|
|
3
|
-
import { compileCheckExpression, expressionColumns, validateDefaultExpression, } from "./query.js";
|
|
3
|
+
import { childExpressions, compileCheckExpression, expressionColumns, hasAggregate, validateDefaultExpression, } from "./query.js";
|
|
4
4
|
import { externalSqlDomainValue, normalizeSqlDomainValue } from "./sql-domains.js";
|
|
5
|
+
const volatileGeneratedFunctions = new Set([
|
|
6
|
+
"CURRENT_DATE",
|
|
7
|
+
"CURRENT_TIMESTAMP",
|
|
8
|
+
"LOCALTIME",
|
|
9
|
+
"RANDOM",
|
|
10
|
+
"GEN_RANDOM_UUID",
|
|
11
|
+
"NEXTVAL",
|
|
12
|
+
"CURRVAL",
|
|
13
|
+
]);
|
|
14
|
+
/** Compiles and validates one immutable, row-local generated-column expression. */
|
|
15
|
+
export function compileGeneratedColumnExpression(tableName, columnName, sql, columns) {
|
|
16
|
+
if (sql.length === 0 || sql.trim() !== sql) {
|
|
17
|
+
throw new TypeError(`Generated SQL must be a trimmed non-empty expression: ${tableName}.${columnName}`);
|
|
18
|
+
}
|
|
19
|
+
const expression = compileCheckExpression(sql, `generated ${tableName}.${columnName}`);
|
|
20
|
+
if (hasAggregate(expression)) {
|
|
21
|
+
throw new TypeError(`Generated column ${tableName}.${columnName} must be an immutable expression over sibling columns`);
|
|
22
|
+
}
|
|
23
|
+
const inspect = (node) => {
|
|
24
|
+
if (node.kind === "subquery" || node.kind === "exists") {
|
|
25
|
+
throw new TypeError(`Generated column ${tableName}.${columnName} must be an immutable expression over sibling columns`);
|
|
26
|
+
}
|
|
27
|
+
if (node.kind === "call" && volatileGeneratedFunctions.has(node.name)) {
|
|
28
|
+
throw new TypeError(`Generated column ${tableName}.${columnName} cannot call volatile function ${node.name}`);
|
|
29
|
+
}
|
|
30
|
+
childExpressions(node).forEach(inspect);
|
|
31
|
+
};
|
|
32
|
+
inspect(expression);
|
|
33
|
+
for (const reference of expressionColumns(expression)) {
|
|
34
|
+
const pieces = reference.split(".");
|
|
35
|
+
const referencedName = pieces.at(-1) ?? reference;
|
|
36
|
+
const qualifier = pieces.length > 1 ? pieces.slice(0, -1).join(".") : undefined;
|
|
37
|
+
if (qualifier !== undefined && qualifier !== tableName) {
|
|
38
|
+
throw new TypeError(`Generated column ${tableName}.${columnName} references another table: ${reference}`);
|
|
39
|
+
}
|
|
40
|
+
const referenced = columns.find(({ name }) => name === referencedName);
|
|
41
|
+
if (referenced === undefined) {
|
|
42
|
+
throw new TypeError(`Generated column ${tableName}.${columnName} names an unknown column: ${referencedName}`);
|
|
43
|
+
}
|
|
44
|
+
if (referencedName === columnName || referenced.generatedValue !== undefined) {
|
|
45
|
+
throw new TypeError(`Generated column ${tableName}.${columnName} cannot reference a generated column: ${referencedName}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return expression;
|
|
49
|
+
}
|
|
5
50
|
/**
|
|
6
51
|
* The constraint name `migrate()` gives a declared relation. It matches the name the SQL parser
|
|
7
52
|
* derives for an unnamed inline REFERENCES, so a table built either way has the same catalog.
|
|
@@ -45,9 +90,11 @@ function createColumn(type, state = {}) {
|
|
|
45
90
|
isNullable: (state.isNullable ?? false),
|
|
46
91
|
isUnique: (state.isUnique ?? false),
|
|
47
92
|
hasDefault: (state.defaultSpec !== undefined),
|
|
93
|
+
isGenerated: (state.generatedSpec !== undefined),
|
|
48
94
|
integer: state.integer ?? false,
|
|
49
95
|
...(state.sqlDomain === undefined ? {} : { sqlDomain: state.sqlDomain }),
|
|
50
96
|
...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
|
|
97
|
+
...(state.generatedSpec === undefined ? {} : { generatedSpec: state.generatedSpec }),
|
|
51
98
|
...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
|
|
52
99
|
...(state.reference === undefined ? {} : { reference: state.reference }),
|
|
53
100
|
...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
|
|
@@ -65,9 +112,15 @@ function createColumn(type, state = {}) {
|
|
|
65
112
|
}),
|
|
66
113
|
renamedFrom: (name) => {
|
|
67
114
|
validateSchemaName(name, "Rename source");
|
|
68
|
-
return createColumn(type, {
|
|
115
|
+
return createColumn(type, {
|
|
116
|
+
...state,
|
|
117
|
+
renamedFromName: name,
|
|
118
|
+
});
|
|
69
119
|
},
|
|
70
|
-
backfill: (value) => createColumn(type, {
|
|
120
|
+
backfill: (value) => createColumn(type, {
|
|
121
|
+
...state,
|
|
122
|
+
backfillValue: value,
|
|
123
|
+
}),
|
|
71
124
|
references: (table, referencedColumn, options = {}) => {
|
|
72
125
|
validateSchemaName(table, "Referenced table");
|
|
73
126
|
validateSchemaName(referencedColumn, "Referenced column");
|
|
@@ -100,6 +153,19 @@ function createColumn(type, state = {}) {
|
|
|
100
153
|
defaultSpec: { kind: "expression", sql: expression },
|
|
101
154
|
});
|
|
102
155
|
}),
|
|
156
|
+
generatedSql: (sql) => {
|
|
157
|
+
const expression = sql.trim();
|
|
158
|
+
if (expression.length === 0 || expression !== sql) {
|
|
159
|
+
throw new TypeError("Generated SQL must be a trimmed non-empty expression");
|
|
160
|
+
}
|
|
161
|
+
if (state.defaultSpec !== undefined) {
|
|
162
|
+
throw new TypeError("A generated column cannot also have a default");
|
|
163
|
+
}
|
|
164
|
+
return createColumn(type, {
|
|
165
|
+
...state,
|
|
166
|
+
generatedSpec: { kind: "stored", sql: expression },
|
|
167
|
+
});
|
|
168
|
+
},
|
|
103
169
|
autoIncrement: (() => {
|
|
104
170
|
if (type !== "number") {
|
|
105
171
|
throw new TypeError("Auto-increment requires a number column");
|
|
@@ -145,6 +211,7 @@ export function columnFromState(state) {
|
|
|
145
211
|
...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
|
|
146
212
|
...(state.reference === undefined ? {} : { reference: state.reference }),
|
|
147
213
|
...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
|
|
214
|
+
...(state.generatedSpec === undefined ? {} : { generatedSpec: state.generatedSpec }),
|
|
148
215
|
...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
|
|
149
216
|
...(state.backfillValue === undefined ? {} : { backfillValue: state.backfillValue }),
|
|
150
217
|
});
|
|
@@ -167,7 +234,9 @@ export const column = {
|
|
|
167
234
|
/** A zoneless calendar date, represented publicly as canonical `YYYY-MM-DD` text. */
|
|
168
235
|
date: () => createColumn("string", { sqlDomain: { kind: "date" } }),
|
|
169
236
|
time: () => createColumn("string", { sqlDomain: { kind: "time" } }),
|
|
170
|
-
interval: () => createColumn("string", {
|
|
237
|
+
interval: () => createColumn("string", {
|
|
238
|
+
sqlDomain: { kind: "interval" },
|
|
239
|
+
}),
|
|
171
240
|
/** JSON array text at the JavaScript boundary, with the SQL element type retained in metadata. */
|
|
172
241
|
array: (element) => createColumn("string", {
|
|
173
242
|
sqlDomain: validateSqlDomain({ kind: "array", element }, "array column"),
|
|
@@ -252,6 +321,10 @@ export function table(name, columns, options = {}) {
|
|
|
252
321
|
if (uniqueEntry?.[1].isNullable === true) {
|
|
253
322
|
throw new TypeError(`Table ${name} unique column must not be nullable: ${uniqueEntry[0]}`);
|
|
254
323
|
}
|
|
324
|
+
const generatedColumns = entries.map(([columnName, definition]) => ({
|
|
325
|
+
name: columnName,
|
|
326
|
+
...(definition.generatedSpec === undefined ? {} : { generatedValue: definition.generatedSpec }),
|
|
327
|
+
}));
|
|
255
328
|
for (const [columnName, definition] of entries) {
|
|
256
329
|
validateSchemaName(columnName, "Column");
|
|
257
330
|
if (definition.integer && definition.type !== "number") {
|
|
@@ -276,6 +349,15 @@ export function table(name, columns, options = {}) {
|
|
|
276
349
|
if (backfill !== undefined && definition.isNullable) {
|
|
277
350
|
throw new TypeError(`A nullable column needs no backfill: ${name}.${columnName}. Rows without it already read NULL.`);
|
|
278
351
|
}
|
|
352
|
+
if (definition.generatedSpec !== undefined) {
|
|
353
|
+
if (definition.defaultSpec !== undefined || backfill !== undefined) {
|
|
354
|
+
throw new TypeError(`A generated column cannot also have a default or backfill: ${name}.${columnName}`);
|
|
355
|
+
}
|
|
356
|
+
compileGeneratedColumnExpression(name, columnName, definition.generatedSpec.sql, generatedColumns);
|
|
357
|
+
if (definition.isUnique || primaryKey.includes(columnName)) {
|
|
358
|
+
throw new TypeError(`Generated columns cannot be row-addressing keys: ${name}.${columnName}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
279
361
|
if (definition.reference?.onDelete === "set null" && !definition.isNullable) {
|
|
280
362
|
throw new TypeError(`ON DELETE SET NULL requires a nullable column: ${name}.${columnName}`);
|
|
281
363
|
}
|
|
@@ -483,6 +565,9 @@ export function view(name, definition) {
|
|
|
483
565
|
if (columnDefinition.defaultSpec !== undefined) {
|
|
484
566
|
throw new TypeError(`A view column cannot have a default: ${name}.${columnName}`);
|
|
485
567
|
}
|
|
568
|
+
if (columnDefinition.generatedSpec !== undefined) {
|
|
569
|
+
throw new TypeError(`A view column cannot be generated: ${name}.${columnName}`);
|
|
570
|
+
}
|
|
486
571
|
}
|
|
487
572
|
return { kind: "view", name, sql: definition.sql, columns: definition.columns };
|
|
488
573
|
}
|
|
@@ -636,6 +721,20 @@ export function assertColumnDroppable(record, column) {
|
|
|
636
721
|
throw new TypeError(`CHECK ${check.name} still uses this column: ${where}`);
|
|
637
722
|
}
|
|
638
723
|
}
|
|
724
|
+
for (const dependent of record.columns) {
|
|
725
|
+
if (dependent.generatedValue === undefined)
|
|
726
|
+
continue;
|
|
727
|
+
let referenced;
|
|
728
|
+
try {
|
|
729
|
+
referenced = expressionColumns(compileCheckExpression(dependent.generatedValue.sql, `generated ${record.name}.${dependent.name}`));
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
throw new TypeError(`Generated column ${dependent.name} cannot be re-read, so ${where} is not droppable`);
|
|
733
|
+
}
|
|
734
|
+
if (referenced.includes(column.name)) {
|
|
735
|
+
throw new TypeError(`Generated column ${dependent.name} still uses this column: ${where}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
639
738
|
}
|
|
640
739
|
function assertColumnRenamable(catalog, record, column) {
|
|
641
740
|
const where = `${record.name}.${column.name}`;
|
|
@@ -654,6 +753,14 @@ function assertColumnRenamable(catalog, record, column) {
|
|
|
654
753
|
throw new TypeError(`CHECK ${check.name} prevents renaming ${where}`);
|
|
655
754
|
}
|
|
656
755
|
}
|
|
756
|
+
for (const dependent of record.columns) {
|
|
757
|
+
if (dependent.generatedValue === undefined)
|
|
758
|
+
continue;
|
|
759
|
+
const referenced = expressionColumns(compileCheckExpression(dependent.generatedValue.sql, `generated ${record.name}.${dependent.name}`));
|
|
760
|
+
if (referenced.includes(column.name)) {
|
|
761
|
+
throw new TypeError(`Generated column ${dependent.name} prevents renaming ${where}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
657
764
|
}
|
|
658
765
|
/**
|
|
659
766
|
* Freezes a column's backfill. A generator runs exactly once — here, while the migration is being
|
|
@@ -793,6 +900,9 @@ export function planMigration(catalog, definition, options = {}) {
|
|
|
793
900
|
}
|
|
794
901
|
}
|
|
795
902
|
if (existing === undefined) {
|
|
903
|
+
if (columnDefinition.generatedSpec !== undefined) {
|
|
904
|
+
throw new TypeError(`Generated columns cannot be added to an existing table without rewriting its rows: ${tableDefinition.name}.${columnName}`);
|
|
905
|
+
}
|
|
796
906
|
const backfill = resolveBackfill(columnDefinition);
|
|
797
907
|
if (!columnDefinition.isNullable && backfill === undefined) {
|
|
798
908
|
throw new TypeError(`Added columns must be nullable, or carry a backfill: ${tableDefinition.name}.${columnName}`);
|
|
@@ -890,13 +1000,24 @@ export function planMigration(catalog, definition, options = {}) {
|
|
|
890
1000
|
columnName,
|
|
891
1001
|
enabled: isAuto,
|
|
892
1002
|
});
|
|
893
|
-
continue;
|
|
894
1003
|
}
|
|
1004
|
+
else {
|
|
1005
|
+
steps.push({
|
|
1006
|
+
kind: "alter-default",
|
|
1007
|
+
tableName: tableDefinition.name,
|
|
1008
|
+
columnName,
|
|
1009
|
+
defaultValue: columnDefinition.defaultSpec ?? null,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
const existingGenerated = existing.generatedValue;
|
|
1014
|
+
const definedGenerated = columnDefinition.generatedSpec;
|
|
1015
|
+
if (JSON.stringify(existingGenerated ?? null) !== JSON.stringify(definedGenerated ?? null)) {
|
|
895
1016
|
steps.push({
|
|
896
|
-
kind: "alter-
|
|
1017
|
+
kind: "alter-generated",
|
|
897
1018
|
tableName: tableDefinition.name,
|
|
898
1019
|
columnName,
|
|
899
|
-
|
|
1020
|
+
generatedValue: definedGenerated ?? null,
|
|
900
1021
|
});
|
|
901
1022
|
}
|
|
902
1023
|
}
|
|
@@ -1184,6 +1305,16 @@ export function applyColumnSteps(record, steps, createId) {
|
|
|
1184
1305
|
else
|
|
1185
1306
|
target.defaultValue = step.defaultValue;
|
|
1186
1307
|
}
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (step.kind === "alter-generated") {
|
|
1311
|
+
const target = columns.find(({ name }) => name === step.columnName);
|
|
1312
|
+
if (target !== undefined) {
|
|
1313
|
+
if (step.generatedValue === null)
|
|
1314
|
+
delete target.generatedValue;
|
|
1315
|
+
else
|
|
1316
|
+
target.generatedValue = step.generatedValue;
|
|
1317
|
+
}
|
|
1187
1318
|
}
|
|
1188
1319
|
}
|
|
1189
1320
|
return columns;
|
|
@@ -18,6 +18,14 @@ export declare function exactNumericCompare(left: unknown, right: unknown): numb
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function boundedJsonText(value: unknown, canonical: boolean, label?: string): string;
|
|
20
20
|
export declare function jsonDomainValue(value: unknown, binary: boolean): string | null;
|
|
21
|
+
/** Returns the JSON document carried by an internal JSON/JSONB scalar, if any. */
|
|
22
|
+
export declare function jsonDomainDocument(value: unknown): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Tags already-constructed JSON without parsing and re-stringifying it. The validation parse
|
|
25
|
+
* rejects malformed documents, while retaining duplicate object names and the constructor's
|
|
26
|
+
* exact member order for embedding in an outer JSON value.
|
|
27
|
+
*/
|
|
28
|
+
export declare function preservedJsonDomainValue(document: string, binary?: boolean): string;
|
|
21
29
|
export declare function uuidDomainValue(value: unknown): string | null;
|
|
22
30
|
/** Canonical, zoneless SQL DATE value. No JavaScript time zone participates in validation. */
|
|
23
31
|
export declare function dateDomainValue(value: unknown): string | null;
|
|
@@ -313,6 +313,31 @@ export function jsonDomainValue(value, binary) {
|
|
|
313
313
|
const text = boundedJsonText(parsed, binary, binary ? "JSONB value" : "JSON value");
|
|
314
314
|
return boundedTaggedDomainValue(binary ? JSONB_VALUE : JSON_VALUE, text, binary ? "JSONB value" : "JSON value");
|
|
315
315
|
}
|
|
316
|
+
/** Returns the JSON document carried by an internal JSON/JSONB scalar, if any. */
|
|
317
|
+
export function jsonDomainDocument(value) {
|
|
318
|
+
if (typeof value !== "string")
|
|
319
|
+
return undefined;
|
|
320
|
+
if (value.startsWith(JSONB_VALUE))
|
|
321
|
+
return value.slice(JSONB_VALUE.length);
|
|
322
|
+
if (value.startsWith(JSON_VALUE))
|
|
323
|
+
return value.slice(JSON_VALUE.length);
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Tags already-constructed JSON without parsing and re-stringifying it. The validation parse
|
|
328
|
+
* rejects malformed documents, while retaining duplicate object names and the constructor's
|
|
329
|
+
* exact member order for embedding in an outer JSON value.
|
|
330
|
+
*/
|
|
331
|
+
export function preservedJsonDomainValue(document, binary = false) {
|
|
332
|
+
assertBoundedDomainString(document, binary ? "JSONB value" : "JSON value");
|
|
333
|
+
try {
|
|
334
|
+
JSON.parse(document);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
throw new TypeError("Invalid JSON value");
|
|
338
|
+
}
|
|
339
|
+
return boundedTaggedDomainValue(binary ? JSONB_VALUE : JSON_VALUE, document, binary ? "JSONB value" : "JSON value");
|
|
340
|
+
}
|
|
316
341
|
export function uuidDomainValue(value) {
|
|
317
342
|
if (value === null || value === undefined)
|
|
318
343
|
return null;
|
package/dist/engine/sql-json.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stringArgument } from "./sql-semantics.js";
|
|
2
|
-
import { externalSqlDomainValue } from "./sql-domains.js";
|
|
2
|
+
import { externalSqlDomainValue, jsonDomainDocument } from "./sql-domains.js";
|
|
3
3
|
import { MAX_CACHEABLE_TEXT_CHARACTERS, MAX_SQL_SCALAR_RESULT_CHARACTERS } from "./cache-limits.js";
|
|
4
4
|
export function parseJsonPath(path, caller) {
|
|
5
5
|
const text = stringArgument(caller, path).trim();
|
|
@@ -119,7 +119,7 @@ export function jsonConstructor(name, values) {
|
|
|
119
119
|
}
|
|
120
120
|
const members = [];
|
|
121
121
|
for (let index = 0; index + 1 < values.length; index += 2) {
|
|
122
|
-
const rawKey = values[index];
|
|
122
|
+
const rawKey = externalSqlDomainValue(values[index]);
|
|
123
123
|
if (rawKey === null || rawKey === undefined) {
|
|
124
124
|
throw new TypeError("JSON_OBJECT keys cannot be NULL");
|
|
125
125
|
}
|
|
@@ -136,12 +136,24 @@ export function jsonConstructor(name, values) {
|
|
|
136
136
|
// Build JSON text directly. WITHOUT UNIQUE KEYS is the default, so duplicate names must be
|
|
137
137
|
// preserved; assigning through a JavaScript object would collapse them and mishandle
|
|
138
138
|
// special names such as "__proto__".
|
|
139
|
-
|
|
139
|
+
// Keys are always JSON strings. In particular, a JSON-domain expression used as a key is
|
|
140
|
+
// its document text, not a raw object member token as JSON-domain values are below.
|
|
141
|
+
const encodedKey = boundedJsonString(key, "JSON_OBJECT key");
|
|
140
142
|
const encodedValue = boundedJsonValue(member ?? null, "JSON_OBJECT value");
|
|
141
143
|
members.push(`${encodedKey}:${encodedValue}`);
|
|
142
144
|
}
|
|
143
145
|
return joinBoundedJson("{", members, "}", "JSON_OBJECT result");
|
|
144
146
|
}
|
|
147
|
+
function boundedJsonString(value, label) {
|
|
148
|
+
if (value.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
|
|
149
|
+
throw new RangeError(`${label} exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
|
|
150
|
+
}
|
|
151
|
+
const encoded = JSON.stringify(value);
|
|
152
|
+
if (encoded.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
|
|
153
|
+
throw new RangeError(`${label} exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
|
|
154
|
+
}
|
|
155
|
+
return encoded;
|
|
156
|
+
}
|
|
145
157
|
function boundedJsonDocument(value, caller) {
|
|
146
158
|
const document = stringArgument(caller, externalSqlDomainValue(value));
|
|
147
159
|
if (document.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
|
|
@@ -150,6 +162,13 @@ function boundedJsonDocument(value, caller) {
|
|
|
150
162
|
return document;
|
|
151
163
|
}
|
|
152
164
|
function boundedJsonValue(value, label) {
|
|
165
|
+
const domainDocument = jsonDomainDocument(value);
|
|
166
|
+
if (domainDocument !== undefined) {
|
|
167
|
+
if (domainDocument.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
|
|
168
|
+
throw new RangeError(`${label} exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
|
|
169
|
+
}
|
|
170
|
+
return domainDocument;
|
|
171
|
+
}
|
|
153
172
|
const normalized = jsonValueOf(value);
|
|
154
173
|
if (typeof normalized === "string" && normalized.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
|
|
155
174
|
throw new RangeError(`${label} exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
|
package/dist/engine/vector.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export interface QuerySpillStore {
|
|
|
75
75
|
export interface AsyncQueryExecutionOptions {
|
|
76
76
|
readonly spillStore?: QuerySpillStore;
|
|
77
77
|
readonly spillPageRows?: number;
|
|
78
|
+
/** Stops before the next execution or spill-storage batch. */
|
|
79
|
+
readonly signal?: AbortSignal;
|
|
78
80
|
/**
|
|
79
81
|
* Makes the scan-source window [start, start + length) resident before each batch. Supplied by
|
|
80
82
|
* a streaming preparation whose scan-source vectors hold only a sliding window; the executor
|
|
@@ -88,8 +90,6 @@ export interface AsyncQueryExecutionOptions {
|
|
|
88
90
|
export interface QueryBatchExecutionOptions extends AsyncQueryExecutionOptions {
|
|
89
91
|
/** Maximum result rows handed to the consumer at once. */
|
|
90
92
|
readonly batchRows: number;
|
|
91
|
-
/** Stops before the next scan batch and is checked after every awaited storage read. */
|
|
92
|
-
readonly signal?: AbortSignal;
|
|
93
93
|
}
|
|
94
94
|
export interface PrepareVectorQueryOptions {
|
|
95
95
|
readonly memoryContext?: QueryMemoryContext;
|