@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
@@ -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 OptionalInsertKeys<TTable extends AnyTable> = NullableKeys<TTable> | DefaultKeys<TTable>;
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> = Omit<{
316
- [K in keyof TTable["columns"]]: ColumnInputValue<TTable["columns"][K]>;
317
- }, OptionalInsertKeys<TTable>> & {
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
  | {
@@ -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, { ...state, renamedFromName: name });
115
+ return createColumn(type, {
116
+ ...state,
117
+ renamedFromName: name,
118
+ });
69
119
  },
70
- backfill: (value) => createColumn(type, { ...state, backfillValue: value }),
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", { sqlDomain: { kind: "interval" } }),
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-default",
1017
+ kind: "alter-generated",
897
1018
  tableName: tableDefinition.name,
898
1019
  columnName,
899
- defaultValue: columnDefinition.defaultSpec ?? null,
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;
@@ -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
- const encodedKey = boundedJsonValue(key, "JSON_OBJECT key");
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`);
@@ -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;