@sequenceholdings/orm 2.0.0 → 2.1.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 +94 -2
- package/dist/authoring.cjs +1474 -54
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +242 -4
- package/dist/authoring.d.ts +242 -4
- package/dist/authoring.js +1465 -56
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +740 -42
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +742 -53
- package/dist/canonical.d.ts +742 -53
- package/dist/canonical.js +724 -43
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +2478 -328
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2480 -330
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +90 -25
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +39 -22
- package/dist/client.d.ts +39 -22
- package/dist/client.js +87 -26
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +1755 -329
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +56 -3
- package/dist/compiler.d.ts +56 -3
- package/dist/compiler.js +1756 -330
- package/dist/compiler.js.map +1 -1
- package/dist/constants-YoFGjIBM.d.cts +99 -0
- package/dist/constants-YoFGjIBM.d.ts +99 -0
- package/dist/dynamic-input.d.cts +1 -0
- package/dist/dynamic-input.d.ts +1 -0
- package/dist/index.cjs +1973 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +474 -100
- package/dist/index.d.ts +474 -100
- package/dist/index.js +1940 -167
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +869 -46
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +699 -25
- package/dist/ir.d.ts +699 -25
- package/dist/ir.js +861 -47
- package/dist/ir.js.map +1 -1
- package/dist/{naming-CTvWSRTf.d.ts → naming-CPfx7EfF.d.ts} +28 -1
- package/dist/{naming-DRyinU4u.d.cts → naming-CU9ThP_E.d.cts} +28 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +852 -49
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +2 -1
- package/dist/predicate-v2.d.ts +2 -1
- package/dist/predicate-v2.js +852 -49
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +1309 -58
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +7 -3
- package/dist/sdl.d.ts +7 -3
- package/dist/sdl.js +1305 -59
- package/dist/sdl.js.map +1 -1
- package/package.json +11 -6
- package/templates/orm-namespace/CLAUDE.md +6 -4
- package/templates/orm-namespace/codegen.preset.d.cts +7 -0
- package/templates/orm-namespace/codegen.ts +2 -1
- package/templates/orm-namespace/graphql/example/queries.ts +4 -3
- package/templates/orm-namespace/package.json +2 -1
- package/templates/orm-namespace/schema/tables.ts +3 -2
- package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { sql, eq, notExists } from 'drizzle-orm';
|
|
2
|
-
import { pgSchema, uniqueIndex, index, foreignKey, primaryKey, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
1
|
+
import { sql, eq, notExists, is, SQL } from 'drizzle-orm';
|
|
2
|
+
import { pgSchema, uniqueIndex, index, foreignKey, check, primaryKey, getTableConfig, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
|
|
4
3
|
import { z } from 'zod';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
5
|
import { createJiti } from 'jiti';
|
|
6
6
|
import { realpath, stat, readdir, readFile, mkdir, writeFile } from 'fs/promises';
|
|
7
7
|
import { createRequire } from 'module';
|
|
@@ -34,10 +34,12 @@ var ColumnBuilder = class {
|
|
|
34
34
|
hasDefault: false,
|
|
35
35
|
defaultValue: void 0,
|
|
36
36
|
defaultFn: void 0,
|
|
37
|
+
onUpdateFn: void 0,
|
|
37
38
|
description: void 0,
|
|
38
39
|
renamedFrom: void 0,
|
|
39
40
|
cast: void 0,
|
|
40
|
-
deprecated: false
|
|
41
|
+
deprecated: false,
|
|
42
|
+
cell: void 0
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
/** Marks this column as (part of) the primary key. Implies NOT NULL. */
|
|
@@ -69,6 +71,15 @@ var ColumnBuilder = class {
|
|
|
69
71
|
this.state.defaultFn = void 0;
|
|
70
72
|
return this;
|
|
71
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Stamp this column with Postgres `now()` on every UPDATE. Only valid on
|
|
76
|
+
* timestamptz/date; the compiler drops it from SetInput so callers cannot
|
|
77
|
+
* supply a value.
|
|
78
|
+
*/
|
|
79
|
+
onUpdate(value) {
|
|
80
|
+
this.state.onUpdateFn = value;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
72
83
|
description(text4) {
|
|
73
84
|
this.state.description = text4;
|
|
74
85
|
return this;
|
|
@@ -88,6 +99,11 @@ var ColumnBuilder = class {
|
|
|
88
99
|
this.state.deprecated = true;
|
|
89
100
|
return this;
|
|
90
101
|
}
|
|
102
|
+
/** @internal System cell-metadata part of logical column `of` (generated resolved views only). */
|
|
103
|
+
cellPart(of, part) {
|
|
104
|
+
this.state.cell = { of, part };
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
91
107
|
/** Whether `.primaryKey()` was called — collected into the table-level PK. */
|
|
92
108
|
get isPrimaryKey() {
|
|
93
109
|
return this.state.primaryKey;
|
|
@@ -112,9 +128,11 @@ var ColumnBuilder = class {
|
|
|
112
128
|
};
|
|
113
129
|
if (this.state.hasDefault) column.default = this.state.defaultValue ?? null;
|
|
114
130
|
if (this.state.defaultFn !== void 0) column.defaultFn = this.state.defaultFn;
|
|
131
|
+
if (this.state.onUpdateFn !== void 0) column.onUpdateFn = this.state.onUpdateFn;
|
|
115
132
|
if (this.state.description !== void 0) column.description = this.state.description;
|
|
116
133
|
if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
|
|
117
134
|
if (this.state.cast !== void 0) column.cast = this.state.cast;
|
|
135
|
+
if (this.state.cell !== void 0) column.cell = { ...this.state.cell };
|
|
118
136
|
return column;
|
|
119
137
|
}
|
|
120
138
|
};
|
|
@@ -138,11 +156,12 @@ var t = {
|
|
|
138
156
|
|
|
139
157
|
// src/constants.ts
|
|
140
158
|
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
141
|
-
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
142
|
-
var MAX_IDENTIFIER_LENGTH = 63;
|
|
143
159
|
var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
|
|
144
160
|
var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
|
|
145
|
-
var
|
|
161
|
+
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
162
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
163
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
164
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
146
165
|
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
147
166
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
148
167
|
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
@@ -154,8 +173,31 @@ var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
|
154
173
|
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
155
174
|
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
156
175
|
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
176
|
+
var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
|
|
177
|
+
var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
|
|
157
178
|
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
158
179
|
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
180
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
181
|
+
var CELL_PART_SUFFIX = {
|
|
182
|
+
at: "__at",
|
|
183
|
+
by: "__by",
|
|
184
|
+
rev: "__rev",
|
|
185
|
+
source: "__src",
|
|
186
|
+
conflicts: "__lost"
|
|
187
|
+
};
|
|
188
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
189
|
+
var CELLS_FIELD = "cells";
|
|
190
|
+
function cellColumnName(column, part) {
|
|
191
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
192
|
+
}
|
|
193
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
194
|
+
var MATERIALIZE_CLAIMS_JSON = JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
195
|
+
function materializedRefreshFunctionName(table) {
|
|
196
|
+
return `${table}__refresh`;
|
|
197
|
+
}
|
|
198
|
+
function materializeLockKey(schema, table) {
|
|
199
|
+
return `orm:materialize:${schema}.${table}`;
|
|
200
|
+
}
|
|
159
201
|
|
|
160
202
|
// src/errors.ts
|
|
161
203
|
var OrmDefinitionError = class extends Error {
|
|
@@ -231,6 +273,8 @@ function toOne(target, options) {
|
|
|
231
273
|
on: options.on,
|
|
232
274
|
references: options.references,
|
|
233
275
|
enforce: options.enforce ?? false,
|
|
276
|
+
onDelete: options.onDelete,
|
|
277
|
+
onUpdate: options.onUpdate,
|
|
234
278
|
description: options.description
|
|
235
279
|
};
|
|
236
280
|
}
|
|
@@ -242,6 +286,8 @@ function toMany(target, options) {
|
|
|
242
286
|
on: options.on,
|
|
243
287
|
references: options.references,
|
|
244
288
|
enforce: false,
|
|
289
|
+
onDelete: void 0,
|
|
290
|
+
onUpdate: void 0,
|
|
245
291
|
description: options.description
|
|
246
292
|
};
|
|
247
293
|
}
|
|
@@ -253,6 +299,8 @@ function toPlatformUser(options) {
|
|
|
253
299
|
on: options.on,
|
|
254
300
|
references: options.references,
|
|
255
301
|
enforce: false,
|
|
302
|
+
onDelete: void 0,
|
|
303
|
+
onUpdate: void 0,
|
|
256
304
|
description: options.description
|
|
257
305
|
};
|
|
258
306
|
}
|
|
@@ -262,10 +310,10 @@ function isSeqLink(value) {
|
|
|
262
310
|
function isSeqTable(value) {
|
|
263
311
|
return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
|
|
264
312
|
}
|
|
265
|
-
function assertIdentifier({ value, what, file }) {
|
|
266
|
-
if (!
|
|
313
|
+
function assertIdentifier({ value, what, file, pattern = IDENTIFIER_PATTERN }) {
|
|
314
|
+
if (!pattern.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
|
|
267
315
|
throw new OrmDefinitionError(
|
|
268
|
-
`${what} "${value}" must match ${
|
|
316
|
+
`${what} "${value}" must match ${pattern} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
|
|
269
317
|
file
|
|
270
318
|
);
|
|
271
319
|
}
|
|
@@ -286,7 +334,13 @@ function defineTable(name, columns, config = {}) {
|
|
|
286
334
|
);
|
|
287
335
|
}
|
|
288
336
|
const columnName = builder.columnNameFor(propertyName);
|
|
289
|
-
assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file });
|
|
337
|
+
assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
|
|
338
|
+
if (config.materializes === void 0 && (propertyName === CELLS_FIELD || Object.values(CELL_PART_SUFFIX).some((suffix) => columnName.endsWith(suffix) || propertyName.endsWith(suffix)))) {
|
|
339
|
+
throw new OrmDefinitionError(
|
|
340
|
+
`table "${name}": "${propertyName}" collides with the reserved cell-metadata names ('${CELLS_FIELD}', or a ${Object.values(CELL_PART_SUFFIX).join("/")} suffix)`,
|
|
341
|
+
file
|
|
342
|
+
);
|
|
343
|
+
}
|
|
290
344
|
const existing = seenColumnNames.get(columnName);
|
|
291
345
|
if (existing !== void 0) {
|
|
292
346
|
throw new OrmDefinitionError(
|
|
@@ -326,7 +380,8 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
|
326
380
|
assertIdentifier({
|
|
327
381
|
value: columnName,
|
|
328
382
|
what: `synced table "${name}": column "${propertyName}" column name`,
|
|
329
|
-
file
|
|
383
|
+
file,
|
|
384
|
+
pattern: COLUMN_IDENTIFIER_PATTERN
|
|
330
385
|
});
|
|
331
386
|
const existing = seenColumnNames.get(columnName);
|
|
332
387
|
if (existing !== void 0) {
|
|
@@ -415,7 +470,7 @@ function defineView(name, columns, config) {
|
|
|
415
470
|
);
|
|
416
471
|
}
|
|
417
472
|
const columnName = builder.columnNameFor(propertyName);
|
|
418
|
-
if (!
|
|
473
|
+
if (!COLUMN_IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
419
474
|
throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
|
|
420
475
|
}
|
|
421
476
|
const existing = seenColumnNames.get(columnName);
|
|
@@ -533,11 +588,11 @@ function compileViewQueries(input) {
|
|
|
533
588
|
);
|
|
534
589
|
}
|
|
535
590
|
if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
|
|
536
|
-
const tableDependencies = extractUsedTable(config.query).map((
|
|
537
|
-
const logical = physicalToLogical.get(
|
|
591
|
+
const tableDependencies = extractUsedTable(config.query).map((physical2) => {
|
|
592
|
+
const logical = physicalToLogical.get(physical2);
|
|
538
593
|
if (!logical) {
|
|
539
594
|
throw new OrmDefinitionError(
|
|
540
|
-
`view "${authored.viewName}" query references unmanaged relation "${
|
|
595
|
+
`view "${authored.viewName}" query references unmanaged relation "${physical2}" \u2014 use relation(...) with a namespace table or view`,
|
|
541
596
|
authored.file
|
|
542
597
|
);
|
|
543
598
|
}
|
|
@@ -861,9 +916,478 @@ function scalarColumnType(table, property, helper) {
|
|
|
861
916
|
}
|
|
862
917
|
return type;
|
|
863
918
|
}
|
|
919
|
+
|
|
920
|
+
// src/compiler/sql.ts
|
|
921
|
+
var CLAIMS_SETTING = "request.jwt.claims";
|
|
922
|
+
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
923
|
+
var ROLES_CLAIM_KEY = "roles";
|
|
924
|
+
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
925
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
926
|
+
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
927
|
+
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
928
|
+
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
929
|
+
var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
930
|
+
|
|
931
|
+
// src/checks/compile.ts
|
|
932
|
+
function tokenize(source) {
|
|
933
|
+
const tokens = [];
|
|
934
|
+
for (let position = 0; position < source.length; ) {
|
|
935
|
+
const char = source[position];
|
|
936
|
+
if (/\s/.test(char)) {
|
|
937
|
+
position++;
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
941
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
942
|
+
position++;
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
if (char === "'") {
|
|
946
|
+
const start = position++;
|
|
947
|
+
let value = "";
|
|
948
|
+
let closed = false;
|
|
949
|
+
while (position < source.length) {
|
|
950
|
+
if (source[position] !== "'") {
|
|
951
|
+
value += source[position++];
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (source[position + 1] === "'") {
|
|
955
|
+
value += "'";
|
|
956
|
+
position += 2;
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
position++;
|
|
960
|
+
closed = true;
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
964
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
965
|
+
tokens.push({ kind: "string", value, position: start });
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
969
|
+
if (operator) {
|
|
970
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
971
|
+
position += operator[0].length;
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
975
|
+
if (number) {
|
|
976
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
977
|
+
position += number[0].length;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
981
|
+
if (identifier3) {
|
|
982
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
983
|
+
position += identifier3[0].length;
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
987
|
+
}
|
|
988
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
989
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
990
|
+
return tokens;
|
|
991
|
+
}
|
|
992
|
+
var Parser = class {
|
|
993
|
+
constructor(tokens, columns) {
|
|
994
|
+
this.tokens = tokens;
|
|
995
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
996
|
+
for (const column of columns) {
|
|
997
|
+
for (const alias of [column.name, column.columnName]) {
|
|
998
|
+
const previous = this.columns.get(alias);
|
|
999
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
1000
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
1001
|
+
}
|
|
1002
|
+
this.columns.set(alias, column);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
index = 0;
|
|
1007
|
+
depth = 0;
|
|
1008
|
+
columns;
|
|
1009
|
+
parse() {
|
|
1010
|
+
const expression = this.parseOr();
|
|
1011
|
+
this.expect("end", "end of expression");
|
|
1012
|
+
return expression;
|
|
1013
|
+
}
|
|
1014
|
+
parseOr() {
|
|
1015
|
+
let left = this.parseAnd();
|
|
1016
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
1017
|
+
return left;
|
|
1018
|
+
}
|
|
1019
|
+
parseAnd() {
|
|
1020
|
+
let left = this.parseNot();
|
|
1021
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
1022
|
+
return left;
|
|
1023
|
+
}
|
|
1024
|
+
parseNot() {
|
|
1025
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
1026
|
+
return this.parsePrimary();
|
|
1027
|
+
}
|
|
1028
|
+
parsePrimary() {
|
|
1029
|
+
if (this.peek().kind === "lparen") {
|
|
1030
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
1031
|
+
this.advance();
|
|
1032
|
+
const expression = this.parseOr();
|
|
1033
|
+
this.expect("rparen", "')'");
|
|
1034
|
+
this.depth--;
|
|
1035
|
+
return `(${expression})`;
|
|
1036
|
+
}
|
|
1037
|
+
return this.parseComparison();
|
|
1038
|
+
}
|
|
1039
|
+
parseComparison() {
|
|
1040
|
+
const left = this.parseValue();
|
|
1041
|
+
if (this.matchKeyword("is")) {
|
|
1042
|
+
const negate = this.matchKeyword("not");
|
|
1043
|
+
this.expectKeyword("null");
|
|
1044
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
1045
|
+
}
|
|
1046
|
+
if (this.matchKeyword("between")) {
|
|
1047
|
+
const lower = this.parseValue();
|
|
1048
|
+
this.expectKeyword("and");
|
|
1049
|
+
const upper = this.parseValue();
|
|
1050
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
1051
|
+
}
|
|
1052
|
+
if (this.matchKeyword("in")) {
|
|
1053
|
+
this.expect("lparen", "'(' after IN");
|
|
1054
|
+
const values = [this.parseValue()];
|
|
1055
|
+
while (this.peek().kind === "comma") {
|
|
1056
|
+
this.advance();
|
|
1057
|
+
values.push(this.parseValue());
|
|
1058
|
+
}
|
|
1059
|
+
this.expect("rparen", "')' after IN values");
|
|
1060
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
1061
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
1062
|
+
}
|
|
1063
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
1064
|
+
const right = this.parseValue();
|
|
1065
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
1066
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
1067
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
1068
|
+
}
|
|
1069
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
1070
|
+
}
|
|
1071
|
+
parseValue() {
|
|
1072
|
+
const token = this.advance();
|
|
1073
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
1074
|
+
if (token.kind === "number") {
|
|
1075
|
+
const number = Number(token.value);
|
|
1076
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
1077
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
1078
|
+
}
|
|
1079
|
+
return { sql: token.value, literal: true };
|
|
1080
|
+
}
|
|
1081
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
1082
|
+
const keyword = token.value.toLowerCase();
|
|
1083
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
1084
|
+
if (keyword === "btrim") {
|
|
1085
|
+
this.expect("lparen", "'(' after btrim");
|
|
1086
|
+
const column2 = this.parseColumn();
|
|
1087
|
+
this.expect("rparen", "')' after btrim column");
|
|
1088
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
1089
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
1090
|
+
}
|
|
1091
|
+
const column = this.columns.get(token.value);
|
|
1092
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
1093
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
1094
|
+
}
|
|
1095
|
+
parseColumn() {
|
|
1096
|
+
const token = this.expect("identifier", "a column name");
|
|
1097
|
+
const column = this.columns.get(token.value);
|
|
1098
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
1099
|
+
return column;
|
|
1100
|
+
}
|
|
1101
|
+
peek() {
|
|
1102
|
+
return this.tokens[this.index];
|
|
1103
|
+
}
|
|
1104
|
+
advance() {
|
|
1105
|
+
const token = this.peek();
|
|
1106
|
+
if (token.kind !== "end") this.index++;
|
|
1107
|
+
return token;
|
|
1108
|
+
}
|
|
1109
|
+
matchKeyword(keyword) {
|
|
1110
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
1111
|
+
this.advance();
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
expectKeyword(keyword) {
|
|
1115
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
1116
|
+
}
|
|
1117
|
+
expect(kind, description) {
|
|
1118
|
+
const token = this.peek();
|
|
1119
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
1120
|
+
return this.advance();
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
function compileCheckExpression(source, columns) {
|
|
1124
|
+
return new Parser(tokenize(source), columns).parse();
|
|
1125
|
+
}
|
|
1126
|
+
var policyVariantSchema = z.object({
|
|
1127
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
1128
|
+
when: z.string().min(1).max(1e3),
|
|
1129
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
1130
|
+
allow: z.literal(true).optional(),
|
|
1131
|
+
policy: z.string().min(1).max(2e3).optional()
|
|
1132
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
1133
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
1134
|
+
});
|
|
1135
|
+
var policyExpressionSchema = z.union([
|
|
1136
|
+
z.string().min(1).max(2e3),
|
|
1137
|
+
z.object({
|
|
1138
|
+
default: z.string().min(1).max(2e3),
|
|
1139
|
+
variants: z.array(policyVariantSchema).max(10).default([])
|
|
1140
|
+
})
|
|
1141
|
+
]);
|
|
1142
|
+
function parseDurationMs(input) {
|
|
1143
|
+
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
1144
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
1145
|
+
if (!match) return void 0;
|
|
1146
|
+
const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
|
|
1147
|
+
const value = Math.round(Number(match[1]) * factor);
|
|
1148
|
+
return value > 0 ? value : void 0;
|
|
1149
|
+
}
|
|
1150
|
+
var identifier = z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
1151
|
+
var durationMs = z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
1152
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
1153
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
1154
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
1155
|
+
};
|
|
1156
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
1157
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
1158
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
1159
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
1160
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
1161
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
1162
|
+
};
|
|
1163
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
1164
|
+
var RESERVED_RESOLVED_COLUMN_NAMES = /* @__PURE__ */ new Set([
|
|
1165
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
1166
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
1167
|
+
]);
|
|
1168
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
1169
|
+
var canonicalFunctionRefSchema = z.object({
|
|
1170
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
1171
|
+
kind: z.enum(["managed", "native"]),
|
|
1172
|
+
id: z.string().min(1).max(200)
|
|
1173
|
+
}).strict();
|
|
1174
|
+
var canonicalFetchShapeSchema = z.union([
|
|
1175
|
+
z.object({ shape: z.literal("single") }).strict(),
|
|
1176
|
+
z.object({ shape: z.literal("batch"), maxKeys: z.number().int().min(2).max(500) }).strict()
|
|
1177
|
+
]);
|
|
1178
|
+
var canonicalResolvedSourceSchema = z.object({
|
|
1179
|
+
name: identifier,
|
|
1180
|
+
kind: z.enum(["manual", "etl", "prefetch"]),
|
|
1181
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
1182
|
+
table: identifier,
|
|
1183
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
1184
|
+
columns: z.array(z.string().min(1).max(63)).min(1).max(500),
|
|
1185
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
1186
|
+
updatedAt: z.string().min(1).max(63).optional(),
|
|
1187
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
1188
|
+
key: z.string().min(1).max(63).optional(),
|
|
1189
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
1190
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
1191
|
+
prefetchDefaults: z.object({
|
|
1192
|
+
maxStalenessMs: durationMs,
|
|
1193
|
+
timeoutMs: durationMs,
|
|
1194
|
+
onError: z.enum(["fallback", "fail"])
|
|
1195
|
+
}).strict().optional(),
|
|
1196
|
+
writeback: z.object({
|
|
1197
|
+
columns: z.array(z.string().min(1).max(63)).min(1).max(500),
|
|
1198
|
+
defaultMode: z.enum(["sync", "async"]),
|
|
1199
|
+
timeoutMs: durationMs,
|
|
1200
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
1201
|
+
clearManualOnConfirm: z.boolean()
|
|
1202
|
+
}).strict().optional()
|
|
1203
|
+
}).strict();
|
|
1204
|
+
var canonicalResolveRuleSchema = z.object({
|
|
1205
|
+
column: z.string().min(1).max(63),
|
|
1206
|
+
comparator: z.enum(COMPARATORS),
|
|
1207
|
+
/** Source names, in tie-break order. */
|
|
1208
|
+
candidates: z.array(identifier).min(1).max(16),
|
|
1209
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
1210
|
+
orderBy: z.string().min(1).max(2e3).optional()
|
|
1211
|
+
}).strict();
|
|
1212
|
+
var canonicalResolvedSchema = z.object({
|
|
1213
|
+
sources: z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
1214
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
1215
|
+
resolve: z.array(canonicalResolveRuleSchema).max(500),
|
|
1216
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
1217
|
+
deletedBy: z.array(identifier).max(1).default([]),
|
|
1218
|
+
/**
|
|
1219
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
1220
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
1221
|
+
* resolution query. Omitted when the view computes on read.
|
|
1222
|
+
*/
|
|
1223
|
+
materialized: z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
1224
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
1225
|
+
policies: z.object({
|
|
1226
|
+
write: policyExpressionSchema.optional(),
|
|
1227
|
+
insert: policyExpressionSchema.optional(),
|
|
1228
|
+
update: policyExpressionSchema.optional()
|
|
1229
|
+
}).strict().default({})
|
|
1230
|
+
}).strict();
|
|
1231
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
1232
|
+
const resolved = view.resolved;
|
|
1233
|
+
if (!resolved) return [];
|
|
1234
|
+
const errors = [];
|
|
1235
|
+
const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1236
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
1237
|
+
if (!pkColumn) {
|
|
1238
|
+
fail("requires exactly one primary-key column");
|
|
1239
|
+
return errors;
|
|
1240
|
+
}
|
|
1241
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
1242
|
+
const scope = {
|
|
1243
|
+
view,
|
|
1244
|
+
resolved,
|
|
1245
|
+
pkColumn,
|
|
1246
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1247
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
1248
|
+
fail
|
|
1249
|
+
};
|
|
1250
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
|
|
1251
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
1252
|
+
for (const source of resolved.sources) {
|
|
1253
|
+
if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
|
|
1254
|
+
scope.sourceByName.set(source.name, source);
|
|
1255
|
+
const table = tableByName.get(source.table);
|
|
1256
|
+
if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1257
|
+
else validateSource({ scope, source, table });
|
|
1258
|
+
}
|
|
1259
|
+
validateRules(scope);
|
|
1260
|
+
validateViewCells(scope);
|
|
1261
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
1262
|
+
for (const name of resolved.deletedBy) {
|
|
1263
|
+
if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
|
|
1264
|
+
}
|
|
1265
|
+
return errors;
|
|
1266
|
+
}
|
|
1267
|
+
function validateSource({ scope, source, table }) {
|
|
1268
|
+
const { fail } = scope;
|
|
1269
|
+
if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
|
|
1270
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
1271
|
+
fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
1272
|
+
}
|
|
1273
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
1274
|
+
if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
1275
|
+
if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
1276
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
1277
|
+
validateSourceColumns({ scope, source, byName });
|
|
1278
|
+
if (source.kind === "etl") {
|
|
1279
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
1280
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
1281
|
+
fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
validatePrefetchFields({ scope, source });
|
|
1285
|
+
}
|
|
1286
|
+
function validatePrefetchFields({ scope, source }) {
|
|
1287
|
+
const { fail, pkColumn } = scope;
|
|
1288
|
+
if (source.kind !== "prefetch") {
|
|
1289
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1290
|
+
fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
1291
|
+
}
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1295
|
+
fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1296
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
1297
|
+
fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1298
|
+
}
|
|
1299
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
1300
|
+
if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
1304
|
+
const { fail, pkColumn, dataColumns } = scope;
|
|
1305
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
1306
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
1307
|
+
const theirs = byName.get(column);
|
|
1308
|
+
if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
|
|
1309
|
+
else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
1310
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
|
|
1314
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
1315
|
+
for (const rule of resolved.resolve) {
|
|
1316
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
1317
|
+
fail(`resolve rule targets unknown column '${rule.column}'`);
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
|
|
1321
|
+
ruled.add(rule.column);
|
|
1322
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
1323
|
+
fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
1324
|
+
}
|
|
1325
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
|
|
1326
|
+
for (const candidate of rule.candidates) {
|
|
1327
|
+
const source = sourceByName.get(candidate);
|
|
1328
|
+
if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
1329
|
+
else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
for (const column of dataColumns.keys()) {
|
|
1333
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
var VIEW_CELL_TYPES = {
|
|
1337
|
+
source: "text",
|
|
1338
|
+
at: "timestamptz",
|
|
1339
|
+
by: "text",
|
|
1340
|
+
conflicts: "jsonb"
|
|
1341
|
+
};
|
|
1342
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail }) {
|
|
1343
|
+
const expected = /* @__PURE__ */ new Map();
|
|
1344
|
+
for (const column of dataColumns.values()) {
|
|
1345
|
+
if (column.name === pkColumn.name) continue;
|
|
1346
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
1347
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
for (const column of view.columns) {
|
|
1351
|
+
if (!column.cell) continue;
|
|
1352
|
+
const want = expected.get(column.name);
|
|
1353
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
1354
|
+
fail(`unexpected cell column '${column.name}'`);
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
1358
|
+
expected.delete(column.name);
|
|
1359
|
+
}
|
|
1360
|
+
for (const name of expected.keys()) fail(`missing cell column '${name}'`);
|
|
1361
|
+
}
|
|
1362
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
1363
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
1364
|
+
const { view, resolved, fail } = scope;
|
|
1365
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
1366
|
+
const table = tableByName.get(tableName);
|
|
1367
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
1368
|
+
fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
1369
|
+
} else {
|
|
1370
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
1371
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
|
|
1372
|
+
}
|
|
1373
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
1374
|
+
if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
|
|
1375
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
|
|
1376
|
+
for (const source of resolved.sources) {
|
|
1377
|
+
if (source.kind !== "etl") continue;
|
|
1378
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
1379
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
1380
|
+
if (!unconditional) {
|
|
1381
|
+
fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/canonical/v2.ts
|
|
864
1387
|
var CANONICAL_FORMAT = 2;
|
|
865
1388
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
866
|
-
var
|
|
1389
|
+
var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
1390
|
+
var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
867
1391
|
var COLUMN_TYPES = [
|
|
868
1392
|
"uuid",
|
|
869
1393
|
"text",
|
|
@@ -878,59 +1402,71 @@ var COLUMN_TYPES = [
|
|
|
878
1402
|
];
|
|
879
1403
|
var columnTypeSchema = z.enum(COLUMN_TYPES);
|
|
880
1404
|
var canonicalEnumSchema = z.object({
|
|
881
|
-
name:
|
|
1405
|
+
name: identifier2,
|
|
882
1406
|
values: z.array(z.string().min(1).max(63)).min(1).max(100)
|
|
883
1407
|
});
|
|
884
1408
|
var canonicalColumnSchema = z.object({
|
|
885
1409
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
886
1410
|
name: z.string().min(1).max(63),
|
|
887
|
-
columnName:
|
|
888
|
-
type: z.union([columnTypeSchema, z.object({ enum:
|
|
1411
|
+
columnName: columnIdentifier,
|
|
1412
|
+
type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
|
|
889
1413
|
nullable: z.boolean().default(true),
|
|
890
1414
|
unique: z.boolean().default(false),
|
|
891
1415
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
892
1416
|
default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
|
|
893
1417
|
defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
1418
|
+
/**
|
|
1419
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
1420
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
1421
|
+
* SetInput / settable so callers cannot backdate it.
|
|
1422
|
+
*/
|
|
1423
|
+
onUpdateFn: z.enum(["now()"]).optional(),
|
|
894
1424
|
description: z.string().max(500).optional(),
|
|
895
1425
|
/** Migration hints — same semantics as v1. */
|
|
896
|
-
renamedFrom:
|
|
1426
|
+
renamedFrom: columnIdentifier.optional(),
|
|
897
1427
|
cast: z.string().max(200).optional(),
|
|
898
|
-
deprecated: z.boolean().default(false)
|
|
1428
|
+
deprecated: z.boolean().default(false),
|
|
1429
|
+
/**
|
|
1430
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
1431
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
1432
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
1433
|
+
* ordinary columns so their hashes are unchanged.
|
|
1434
|
+
*/
|
|
1435
|
+
cell: z.object({ of: z.string().min(1).max(63), part: z.enum(CELL_PARTS) }).strict().optional()
|
|
1436
|
+
}).superRefine((column, ctx) => {
|
|
1437
|
+
if (column.onUpdateFn === void 0) return;
|
|
1438
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
1439
|
+
ctx.addIssue({
|
|
1440
|
+
code: "custom",
|
|
1441
|
+
path: ["onUpdateFn"],
|
|
1442
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
899
1445
|
});
|
|
900
1446
|
var canonicalLinkSchema = z.object({
|
|
901
1447
|
name: z.string().min(1).max(63),
|
|
902
1448
|
kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
903
1449
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
904
|
-
target:
|
|
1450
|
+
target: identifier2.optional(),
|
|
905
1451
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
906
|
-
on:
|
|
1452
|
+
on: columnIdentifier,
|
|
907
1453
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
908
|
-
references:
|
|
1454
|
+
references: columnIdentifier.optional(),
|
|
909
1455
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
910
1456
|
enforce: z.boolean().default(false),
|
|
1457
|
+
onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
1458
|
+
onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
911
1459
|
description: z.string().max(500).optional()
|
|
912
1460
|
});
|
|
913
1461
|
var canonicalIndexSchema = z.object({
|
|
914
|
-
columns: z.array(
|
|
1462
|
+
columns: z.array(columnIdentifier).min(1),
|
|
915
1463
|
unique: z.boolean().default(false),
|
|
916
|
-
name:
|
|
1464
|
+
name: identifier2.optional()
|
|
917
1465
|
});
|
|
918
|
-
var
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
allow: z.literal(true).optional(),
|
|
923
|
-
policy: z.string().min(1).max(2e3).optional()
|
|
924
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
925
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
926
|
-
});
|
|
927
|
-
var policyExpressionSchema = z.union([
|
|
928
|
-
z.string().min(1).max(2e3),
|
|
929
|
-
z.object({
|
|
930
|
-
default: z.string().min(1).max(2e3),
|
|
931
|
-
variants: z.array(policyVariantSchema).max(10).default([])
|
|
932
|
-
})
|
|
933
|
-
]);
|
|
1466
|
+
var canonicalCheckSchema = z.object({
|
|
1467
|
+
name: identifier2,
|
|
1468
|
+
expression: z.string().min(1).max(2e3)
|
|
1469
|
+
}).strict();
|
|
934
1470
|
var canonicalPoliciesSchema = z.object({
|
|
935
1471
|
read: policyExpressionSchema.optional(),
|
|
936
1472
|
write: policyExpressionSchema.optional(),
|
|
@@ -1118,7 +1654,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
|
1118
1654
|
"width_bucket"
|
|
1119
1655
|
];
|
|
1120
1656
|
var canonicalActionSchema = z.object({
|
|
1121
|
-
name:
|
|
1657
|
+
name: identifier2,
|
|
1122
1658
|
requires: z.string().max(1e3).optional(),
|
|
1123
1659
|
params: z.record(z.string(), z.object({
|
|
1124
1660
|
type: columnTypeSchema,
|
|
@@ -1130,9 +1666,9 @@ var canonicalActionSchema = z.object({
|
|
|
1130
1666
|
});
|
|
1131
1667
|
var canonicalCowMutationSchema = z.object({
|
|
1132
1668
|
kind: z.enum(["override", "revert"]),
|
|
1133
|
-
name:
|
|
1669
|
+
name: identifier2,
|
|
1134
1670
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
1135
|
-
synced:
|
|
1671
|
+
synced: identifier2,
|
|
1136
1672
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
1137
1673
|
pk: z.string().min(1).max(63),
|
|
1138
1674
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -1159,17 +1695,28 @@ var canonicalCowMutationSchema = z.object({
|
|
|
1159
1695
|
}
|
|
1160
1696
|
}
|
|
1161
1697
|
});
|
|
1698
|
+
var canonicalForeignKeySchema = z.object({
|
|
1699
|
+
name: identifier2,
|
|
1700
|
+
columns: z.array(columnIdentifier).min(1).max(8),
|
|
1701
|
+
target: identifier2,
|
|
1702
|
+
references: z.array(columnIdentifier).min(1).max(8),
|
|
1703
|
+
deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
1704
|
+
onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
1705
|
+
onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
1706
|
+
}).strict();
|
|
1162
1707
|
var canonicalTableSchema = z.object({
|
|
1163
|
-
table:
|
|
1708
|
+
table: identifier2,
|
|
1164
1709
|
description: z.string().max(2e3).optional(),
|
|
1165
1710
|
/** Table-level rename hint (new in v2). */
|
|
1166
|
-
renamedFrom:
|
|
1711
|
+
renamedFrom: identifier2.optional(),
|
|
1167
1712
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
1168
1713
|
source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
1169
1714
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
1170
|
-
primaryKey: z.array(
|
|
1715
|
+
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1171
1716
|
links: z.array(canonicalLinkSchema).default([]),
|
|
1172
1717
|
indexes: z.array(canonicalIndexSchema).default([]),
|
|
1718
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1719
|
+
checks: z.array(canonicalCheckSchema).max(100).optional(),
|
|
1173
1720
|
policies: canonicalPoliciesSchema.default({}),
|
|
1174
1721
|
/**
|
|
1175
1722
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -1178,26 +1725,64 @@ var canonicalTableSchema = z.object({
|
|
|
1178
1725
|
*/
|
|
1179
1726
|
actions: z.array(canonicalActionSchema).default([]),
|
|
1180
1727
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
1181
|
-
cowMutations: z.array(canonicalCowMutationSchema).optional()
|
|
1728
|
+
cowMutations: z.array(canonicalCowMutationSchema).optional(),
|
|
1729
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1730
|
+
foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
1731
|
+
/**
|
|
1732
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
1733
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
1734
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
1735
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
1736
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
1737
|
+
*/
|
|
1738
|
+
cellMetadata: z.literal(true).optional(),
|
|
1739
|
+
/**
|
|
1740
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
1741
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
1742
|
+
* only the data plane writes them. Omitted when false.
|
|
1743
|
+
*/
|
|
1744
|
+
internal: z.literal(true).optional(),
|
|
1745
|
+
/**
|
|
1746
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
1747
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
1748
|
+
* data plane. Omitted otherwise.
|
|
1749
|
+
*/
|
|
1750
|
+
materializes: identifier2.optional()
|
|
1182
1751
|
});
|
|
1752
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
1753
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
1183
1754
|
var canonicalViewSchema = z.object({
|
|
1184
|
-
view:
|
|
1755
|
+
view: identifier2,
|
|
1185
1756
|
description: z.string().max(2e3).optional(),
|
|
1186
1757
|
columns: z.array(canonicalColumnSchema).min(1),
|
|
1187
1758
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
1188
|
-
primaryKey: z.array(
|
|
1759
|
+
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1189
1760
|
links: z.array(canonicalLinkSchema).default([]),
|
|
1190
1761
|
policies: z.object({ read: policyExpressionSchema }).strict(),
|
|
1191
|
-
query: z.string().min(1).max(
|
|
1762
|
+
query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
1192
1763
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
1193
1764
|
}),
|
|
1194
1765
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
1195
|
-
dependencies: z.array(
|
|
1766
|
+
dependencies: z.array(identifier2).max(500),
|
|
1767
|
+
/**
|
|
1768
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
1769
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
1770
|
+
* existing views hash unchanged.
|
|
1771
|
+
*/
|
|
1772
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
1773
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
1774
|
+
internal: z.literal(true).optional()
|
|
1196
1775
|
}).superRefine((view, ctx) => {
|
|
1776
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
1777
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
1778
|
+
}
|
|
1197
1779
|
for (const [index3, column] of view.columns.entries()) {
|
|
1198
1780
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1199
1781
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
1200
1782
|
}
|
|
1783
|
+
if (column.onUpdateFn !== void 0) {
|
|
1784
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
1785
|
+
}
|
|
1201
1786
|
if (column.unique) {
|
|
1202
1787
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
|
|
1203
1788
|
}
|
|
@@ -1205,15 +1790,82 @@ var canonicalViewSchema = z.object({
|
|
|
1205
1790
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
|
|
1206
1791
|
}
|
|
1207
1792
|
}
|
|
1793
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
1794
|
+
for (const [index3, column] of view.columns.entries()) {
|
|
1795
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index3], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1208
1798
|
for (const [index3, link] of view.links.entries()) {
|
|
1209
1799
|
if (link.enforce) {
|
|
1210
1800
|
ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
1211
1801
|
}
|
|
1802
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
1803
|
+
ctx.addIssue({ code: "custom", path: ["links", index3], message: `view link '${link.name}' cannot declare referential actions` });
|
|
1804
|
+
}
|
|
1212
1805
|
}
|
|
1213
1806
|
});
|
|
1807
|
+
var CELL_PART_TYPES = {
|
|
1808
|
+
at: "timestamptz",
|
|
1809
|
+
by: "text",
|
|
1810
|
+
rev: "uuid",
|
|
1811
|
+
source: "text",
|
|
1812
|
+
conflicts: "jsonb"
|
|
1813
|
+
};
|
|
1814
|
+
function trackedCellColumns(table) {
|
|
1815
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
1816
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
1817
|
+
}
|
|
1818
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1819
|
+
function validateCellMetadata(table, index3, ctx) {
|
|
1820
|
+
const fail = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
|
|
1821
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
1822
|
+
if (table.materializes !== void 0) return;
|
|
1823
|
+
if (table.cellMetadata !== true) {
|
|
1824
|
+
for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index3, "columns", position]);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (table.source !== void 0) {
|
|
1828
|
+
fail(`synced table '${table.table}' cannot track cell metadata`);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
const tracked = trackedCellColumns(table);
|
|
1832
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
1833
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
1834
|
+
if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
1835
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1836
|
+
for (const { column, position, cell } of cells) {
|
|
1837
|
+
const path = ["tables", index3, "columns", position];
|
|
1838
|
+
const owner = byName.get(cell.of);
|
|
1839
|
+
if (!owner) {
|
|
1840
|
+
fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
1844
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
1845
|
+
fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
1846
|
+
}
|
|
1847
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1848
|
+
fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
1849
|
+
}
|
|
1850
|
+
const key = `${cell.of}:${cell.part}`;
|
|
1851
|
+
if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
|
|
1852
|
+
seen.add(key);
|
|
1853
|
+
}
|
|
1854
|
+
for (const column of tracked) {
|
|
1855
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
1856
|
+
fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
1857
|
+
}
|
|
1858
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
1859
|
+
fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
1860
|
+
}
|
|
1861
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
1862
|
+
if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1214
1866
|
var canonicalDefinitionSchema = z.object({
|
|
1215
1867
|
definition_format: z.literal(CANONICAL_FORMAT),
|
|
1216
|
-
namespace:
|
|
1868
|
+
namespace: identifier2.max(41),
|
|
1217
1869
|
description: z.string().max(2e3).optional(),
|
|
1218
1870
|
enums: z.array(canonicalEnumSchema).default([]),
|
|
1219
1871
|
tables: z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -1231,6 +1883,74 @@ var canonicalDefinitionSchema = z.object({
|
|
|
1231
1883
|
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
1232
1884
|
}
|
|
1233
1885
|
seen.add(table.table);
|
|
1886
|
+
validateCellMetadata(table, index3, ctx);
|
|
1887
|
+
if (table.source !== void 0) {
|
|
1888
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1889
|
+
if (column.onUpdateFn !== void 0) {
|
|
1890
|
+
ctx.addIssue({
|
|
1891
|
+
code: "custom",
|
|
1892
|
+
path: ["tables", index3, "columns", columnIndex],
|
|
1893
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1900
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1901
|
+
const names = /* @__PURE__ */ new Set([
|
|
1902
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
1903
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
1904
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
1905
|
+
]);
|
|
1906
|
+
for (const [index3, link] of table.links.entries()) {
|
|
1907
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
1908
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "referential actions require an enforced toOne link" });
|
|
1909
|
+
}
|
|
1910
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
1911
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
1912
|
+
if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "SET NULL requires a nullable source column" });
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
for (const [index3, check] of (table.checks ?? []).entries()) {
|
|
1916
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index3], message });
|
|
1917
|
+
if (table.source) fail("synced tables cannot declare checks");
|
|
1918
|
+
if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
|
|
1919
|
+
names.add(check.name);
|
|
1920
|
+
try {
|
|
1921
|
+
compileCheckExpression(check.expression, table.columns);
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
1927
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
|
|
1928
|
+
if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
|
|
1929
|
+
names.add(fk.name);
|
|
1930
|
+
const target = fkTables.get(fk.target);
|
|
1931
|
+
if (table.source || !target || target.source) {
|
|
1932
|
+
fail("foreign keys must connect owned tables in the same namespace");
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
|
|
1936
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
|
|
1937
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
1938
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
1939
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
1940
|
+
if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
|
|
1941
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
|
|
1942
|
+
}
|
|
1943
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
1944
|
+
if (fk.columns.some((name) => table.columns.find((column) => column.columnName === name)?.nullable === false || table.primaryKey?.includes(name))) fail("SET NULL requires nullable source columns");
|
|
1945
|
+
}
|
|
1946
|
+
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
1947
|
+
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
1951
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
1952
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
1953
|
+
}
|
|
1234
1954
|
}
|
|
1235
1955
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1236
1956
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -1355,8 +2075,12 @@ function canonicalRelations(definition) {
|
|
|
1355
2075
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
1356
2076
|
links: view.links,
|
|
1357
2077
|
indexes: [],
|
|
1358
|
-
|
|
1359
|
-
|
|
2078
|
+
// A resolved table's write policies (evaluated against the resolved row)
|
|
2079
|
+
// ride alongside the view's read policy.
|
|
2080
|
+
policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
|
|
2081
|
+
actions: [],
|
|
2082
|
+
...view.resolved !== void 0 ? { resolved: view.resolved } : {},
|
|
2083
|
+
...view.internal ? { internal: true } : {}
|
|
1360
2084
|
}));
|
|
1361
2085
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
1362
2086
|
}
|
|
@@ -1366,12 +2090,14 @@ function normalizeCanonical(definition) {
|
|
|
1366
2090
|
...definition,
|
|
1367
2091
|
enums: sortByName(definition.enums),
|
|
1368
2092
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
1369
|
-
const { cowMutations, ...rest } = t2;
|
|
2093
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t2;
|
|
1370
2094
|
return {
|
|
1371
2095
|
...rest,
|
|
1372
2096
|
links: sortByName(t2.links),
|
|
1373
2097
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
1374
2098
|
actions: sortByName(t2.actions),
|
|
2099
|
+
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
2100
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
1375
2101
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
1376
2102
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
1377
2103
|
};
|
|
@@ -1386,6 +2112,454 @@ function normalizeCanonical(definition) {
|
|
|
1386
2112
|
}
|
|
1387
2113
|
return normalized;
|
|
1388
2114
|
}
|
|
2115
|
+
|
|
2116
|
+
// src/authoring/resolved-sources.ts
|
|
2117
|
+
function cloneColumn({ builder, property, notNull, primaryKey: primaryKey2 }) {
|
|
2118
|
+
const canonical = builder.toCanonicalColumn(property);
|
|
2119
|
+
let clone = new ColumnBuilder(canonical.type, canonical.columnName);
|
|
2120
|
+
if (primaryKey2) clone = clone.primaryKey();
|
|
2121
|
+
if (notNull) clone = clone.notNull();
|
|
2122
|
+
if (canonical.description !== void 0) clone = clone.description(canonical.description);
|
|
2123
|
+
return clone;
|
|
2124
|
+
}
|
|
2125
|
+
function planSource(input) {
|
|
2126
|
+
const { sourceName, input: source, fail } = input;
|
|
2127
|
+
if (!IDENTIFIER_PATTERN.test(sourceName)) fail(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
|
|
2128
|
+
switch (source.kind) {
|
|
2129
|
+
case "manual":
|
|
2130
|
+
return {
|
|
2131
|
+
name: sourceName,
|
|
2132
|
+
kind: "manual",
|
|
2133
|
+
columns: input.dataColumns,
|
|
2134
|
+
table: internalTable(input, input.dataColumns, {
|
|
2135
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned(),
|
|
2136
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName),
|
|
2137
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName),
|
|
2138
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.columnName)
|
|
2139
|
+
}, { revisions: true })
|
|
2140
|
+
};
|
|
2141
|
+
case "etl":
|
|
2142
|
+
return planEtl(input, source);
|
|
2143
|
+
case "prefetch":
|
|
2144
|
+
return planPrefetch(input, source);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
|
|
2148
|
+
function internalTable(input, supplied, system, options = {}) {
|
|
2149
|
+
const { viewName, sourceName, columns, pk, pkBuilder } = input;
|
|
2150
|
+
const tableColumns = { [pk]: cloneColumn({ builder: pkBuilder, property: pk, notNull: true, primaryKey: true }) };
|
|
2151
|
+
for (const property of supplied) {
|
|
2152
|
+
tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
|
|
2153
|
+
}
|
|
2154
|
+
return defineTable(`${viewName}__${sourceName}`, { ...tableColumns, ...system }, {
|
|
2155
|
+
description: `Internal ${input.input.kind} source "${sourceName}" of resolved table "${viewName}". System-maintained.`,
|
|
2156
|
+
cellMetadata: true,
|
|
2157
|
+
// The manual source identifies each edit (`c__rev`) so an async
|
|
2158
|
+
// confirmation clears only the exact edit it delivered.
|
|
2159
|
+
...options.revisions ? { cellRevisions: true } : {},
|
|
2160
|
+
internal: true
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
function assertColumns(input, list, what) {
|
|
2164
|
+
for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2165
|
+
}
|
|
2166
|
+
function planEtl(input, source) {
|
|
2167
|
+
const { sourceName, pk, dataColumns, fail } = input;
|
|
2168
|
+
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2169
|
+
if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2170
|
+
const synced = table;
|
|
2171
|
+
if (!(pk in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
|
|
2172
|
+
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2173
|
+
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2174
|
+
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
2175
|
+
fail(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
|
|
2176
|
+
}
|
|
2177
|
+
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2178
|
+
}
|
|
2179
|
+
function planPrefetch(input, source) {
|
|
2180
|
+
const { sourceName, pk, fail } = input;
|
|
2181
|
+
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2182
|
+
if (source.key !== pk && !source.columns.includes(source.key)) {
|
|
2183
|
+
fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2184
|
+
}
|
|
2185
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
2186
|
+
if (!source.columns.includes(column)) fail(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
|
|
2187
|
+
}
|
|
2188
|
+
const writeback = source.writeback;
|
|
2189
|
+
return {
|
|
2190
|
+
name: sourceName,
|
|
2191
|
+
kind: "prefetch",
|
|
2192
|
+
columns: [...source.columns],
|
|
2193
|
+
key: source.key,
|
|
2194
|
+
function: source.function,
|
|
2195
|
+
fetch: source.fetch === "single" ? { shape: "single" } : { shape: "batch", maxKeys: source.fetch.batch.maxKeys },
|
|
2196
|
+
prefetchDefaults: {
|
|
2197
|
+
maxStalenessMs: parseDuration(source.prefetchDefaults?.maxStaleness ?? "30s"),
|
|
2198
|
+
timeoutMs: parseDuration(source.prefetchDefaults?.timeout ?? "1500ms"),
|
|
2199
|
+
onError: source.prefetchDefaults?.onError ?? "fallback"
|
|
2200
|
+
},
|
|
2201
|
+
...writeback ? {
|
|
2202
|
+
writeback: {
|
|
2203
|
+
columns: [...writeback.columns],
|
|
2204
|
+
defaultMode: writeback.defaultMode ?? "sync",
|
|
2205
|
+
timeoutMs: parseDuration(writeback.timeout ?? "5s"),
|
|
2206
|
+
clearManualOnConfirm: writeback.clearManualOnConfirm ?? true
|
|
2207
|
+
}
|
|
2208
|
+
} : {},
|
|
2209
|
+
table: internalTable(input, source.columns, {
|
|
2210
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.columnName).notNull().default("now()"),
|
|
2211
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned()
|
|
2212
|
+
})
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
function parseDuration(input) {
|
|
2216
|
+
const value = parseDurationMs(input);
|
|
2217
|
+
if (value === void 0) throw new OrmDefinitionError(`invalid duration "${input}" \u2014 use e.g. '1500ms', '30s', '5m'`);
|
|
2218
|
+
return value;
|
|
2219
|
+
}
|
|
2220
|
+
var PG_TYPE = {
|
|
2221
|
+
uuid: "uuid",
|
|
2222
|
+
text: "text",
|
|
2223
|
+
integer: "integer",
|
|
2224
|
+
bigint: "bigint",
|
|
2225
|
+
numeric: "numeric",
|
|
2226
|
+
double: "double precision",
|
|
2227
|
+
boolean: "boolean",
|
|
2228
|
+
timestamptz: "timestamptz",
|
|
2229
|
+
date: "date",
|
|
2230
|
+
jsonb: "jsonb"
|
|
2231
|
+
};
|
|
2232
|
+
var ident = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
2233
|
+
var literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
2234
|
+
var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
|
|
2235
|
+
function candidateSql({ source, column, pk, pgType }) {
|
|
2236
|
+
const a = ident(`s_${source.name}`);
|
|
2237
|
+
const name = physical({ source, property: column });
|
|
2238
|
+
const value = `CAST(${a}.${ident(name)} AS ${pgType})`;
|
|
2239
|
+
if (source.kind === "etl") {
|
|
2240
|
+
return {
|
|
2241
|
+
source: source.name,
|
|
2242
|
+
value,
|
|
2243
|
+
has: `(${a}.${ident(physical({ source, property: pk }))} IS NOT NULL)`,
|
|
2244
|
+
at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
|
|
2245
|
+
by: "CAST(NULL AS text)"
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
const at = `${a}.${ident(cellColumnName(name, "at"))}`;
|
|
2249
|
+
const tombstoned2 = source.kind === "prefetch" ? ` AND NOT ${a}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName)}` : "";
|
|
2250
|
+
return { source: source.name, value, has: `(${at} IS NOT NULL${tombstoned2})`, at, by: `${a}.${ident(cellColumnName(name, "by"))}` };
|
|
2251
|
+
}
|
|
2252
|
+
function outranks({ i, j, key, direction, inclusive }) {
|
|
2253
|
+
const ki = i[key];
|
|
2254
|
+
const kj = j[key];
|
|
2255
|
+
const op = inclusive ? `${direction}=` : direction;
|
|
2256
|
+
return inclusive ? `(${kj} IS NULL OR (${ki} IS NOT NULL AND ${ki} ${op} ${kj}))` : `(${ki} IS NOT NULL AND (${kj} IS NULL OR ${ki} ${op} ${kj}))`;
|
|
2257
|
+
}
|
|
2258
|
+
function rankedWinner({ candidates, key, direction }) {
|
|
2259
|
+
const branches = candidates.map((candidate, index3) => {
|
|
2260
|
+
const beats = candidates.map((other, otherIndex) => otherIndex === index3 ? null : `(NOT ${other.has} OR ${outranks({ i: candidate, j: other, key, direction, inclusive: index3 < otherIndex })})`).filter((clause) => clause !== null);
|
|
2261
|
+
return `WHEN ${[candidate.has, ...beats].join(" AND ")} THEN ${index3 + 1}`;
|
|
2262
|
+
});
|
|
2263
|
+
return `CASE ${branches.join(" ")} END`;
|
|
2264
|
+
}
|
|
2265
|
+
function winnerIndex({ comparator, candidates }) {
|
|
2266
|
+
const firstWith = (test) => candidates.map((candidate, index3) => `WHEN ${test(candidate)} THEN ${index3 + 1}`).join(" ");
|
|
2267
|
+
if (candidates.length === 1 || comparator === "priority") return `CASE ${firstWith((c) => c.has)} END`;
|
|
2268
|
+
switch (comparator) {
|
|
2269
|
+
case "latest":
|
|
2270
|
+
return rankedWinner({ candidates, key: "at", direction: ">" });
|
|
2271
|
+
case "highest":
|
|
2272
|
+
return rankedWinner({ candidates, key: "value", direction: ">" });
|
|
2273
|
+
case "lowest":
|
|
2274
|
+
return rankedWinner({ candidates, key: "value", direction: "<" });
|
|
2275
|
+
case "nonNullFirst":
|
|
2276
|
+
return `CASE ${firstWith((c) => `${c.has} AND ${c.value} IS NOT NULL`)} ${firstWith((c) => c.has)} END`;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
function pick(winner, candidates, field) {
|
|
2280
|
+
return `CASE ${winner} ${candidates.map((candidate, index3) => `WHEN ${index3 + 1} THEN ${field(candidate)}`).join(" ")} END`;
|
|
2281
|
+
}
|
|
2282
|
+
function loserSql({ loser, winner }) {
|
|
2283
|
+
return `CASE WHEN ${loser.has} AND ${loser.value} IS DISTINCT FROM ${winner.value} AND ${loser.at} > ${winner.at} THEN jsonb_build_array(jsonb_build_object('source', ${literal(loser.source)}, 'value', to_jsonb(${loser.value}), 'at', ${loser.at})) ELSE '[]'::jsonb END`;
|
|
2284
|
+
}
|
|
2285
|
+
function losersAgainst({ candidates, winner, isWinner }) {
|
|
2286
|
+
const losers = candidates.flatMap((loser, index3) => {
|
|
2287
|
+
const excluded = isWinner(loser, index3);
|
|
2288
|
+
if (excluded === true) return [];
|
|
2289
|
+
const clause = loserSql({ loser, winner });
|
|
2290
|
+
return [excluded === false ? clause : `CASE WHEN ${excluded} THEN '[]'::jsonb ELSE ${clause} END`];
|
|
2291
|
+
});
|
|
2292
|
+
return losers.length === 0 ? "CAST(NULL AS jsonb)" : `NULLIF(${losers.join(" || ")}, '[]'::jsonb)`;
|
|
2293
|
+
}
|
|
2294
|
+
function builtinCell({ comparator, candidates }) {
|
|
2295
|
+
const winner = winnerIndex({ comparator, candidates });
|
|
2296
|
+
const conflicts = comparator === "latest" || candidates.length < 2 ? "CAST(NULL AS jsonb)" : `CASE ${winner} ${candidates.map((known, index3) => `WHEN ${index3 + 1} THEN ${losersAgainst({ candidates, winner: known, isWinner: (_c, other) => other === index3 })}`).join(" ")} END`;
|
|
2297
|
+
return {
|
|
2298
|
+
value: pick(winner, candidates, (c) => c.value),
|
|
2299
|
+
parts: {
|
|
2300
|
+
source: pick(winner, candidates, (c) => literal(c.source)),
|
|
2301
|
+
at: pick(winner, candidates, (c) => c.at),
|
|
2302
|
+
by: pick(winner, candidates, (c) => c.by),
|
|
2303
|
+
conflicts
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
function customCell({ candidates, orderBy, index: index3 }) {
|
|
2308
|
+
const w = ident(`w_${index3}`);
|
|
2309
|
+
const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
|
|
2310
|
+
const lateral = ` LEFT JOIN LATERAL (SELECT max(winner.source) AS source, (array_agg(winner.value))[1] AS value, max(winner.updated_at) AS updated_at, max(winner.actor) AS actor FROM (WITH cand(source, has, value, updated_at, actor, ord) AS (VALUES ${rows.join(", ")}) SELECT c.source, c.value, c.updated_at, c.actor FROM cand c WHERE c.has ORDER BY ${orderBy}, c.ord LIMIT 1) AS winner) AS ${w} ON true`;
|
|
2311
|
+
const winner = { value: `${w}.value`, at: `${w}.updated_at` };
|
|
2312
|
+
return {
|
|
2313
|
+
value: winner.value,
|
|
2314
|
+
lateral,
|
|
2315
|
+
parts: {
|
|
2316
|
+
source: `${w}.source`,
|
|
2317
|
+
at: winner.at,
|
|
2318
|
+
by: `${w}.actor`,
|
|
2319
|
+
conflicts: candidates.length < 2 ? "CAST(NULL AS jsonb)" : losersAgainst({ candidates, winner, isWinner: (c) => `${w}.source = ${literal(c.source)}` })
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
function buildResolvedQuery({ context, plan }) {
|
|
2324
|
+
const { qb, relation } = context;
|
|
2325
|
+
const pkColumn = plan.columns[plan.pk].columnNameFor(plan.pk);
|
|
2326
|
+
const alias = (source) => ident(`s_${source}`);
|
|
2327
|
+
const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
|
|
2328
|
+
const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
|
|
2329
|
+
const schema = getTableConfig(relation(plan.plans[0].table)).schema;
|
|
2330
|
+
const keyBranches = plan.plans.map((source) => {
|
|
2331
|
+
const where = source.kind === "prefetch" ? sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : sql.raw("");
|
|
2332
|
+
const sourcePk = ident(physical({ source, property: plan.pk }));
|
|
2333
|
+
const selectedPk = sourcePk === ident(pkColumn) ? sourcePk : `${sourcePk} AS ${ident(pkColumn)}`;
|
|
2334
|
+
return sql`SELECT ${sql.raw(selectedPk)} FROM ${relation(source.table)}${where}`;
|
|
2335
|
+
});
|
|
2336
|
+
const joins = plan.plans.map(
|
|
2337
|
+
(source) => sql` LEFT JOIN ${relation(source.table)} AS ${sql.raw(alias(source.name))} ON ${sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
|
|
2338
|
+
);
|
|
2339
|
+
const cells = /* @__PURE__ */ new Map();
|
|
2340
|
+
plan.rules.forEach((rule, index3) => {
|
|
2341
|
+
const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
|
|
2342
|
+
const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
|
|
2343
|
+
const candidates = rule.candidates.map((name) => candidateSql({ source: sourceByName.get(name), column: rule.column, pk: plan.pk, pgType }));
|
|
2344
|
+
cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index: index3 }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2345
|
+
});
|
|
2346
|
+
const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [sql.raw(cell.lateral)] : []);
|
|
2347
|
+
const from = sql.join([sql`(${sql.join(keyBranches, sql.raw(" UNION "))}) AS "k"`, ...joins, ...laterals], sql.raw(""));
|
|
2348
|
+
const selected = {
|
|
2349
|
+
[plan.pk]: sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
|
|
2350
|
+
};
|
|
2351
|
+
for (const [property, builder] of Object.entries(plan.columns)) {
|
|
2352
|
+
if (property === plan.pk) continue;
|
|
2353
|
+
const cell = cells.get(property);
|
|
2354
|
+
const columnName = builder.columnNameFor(property);
|
|
2355
|
+
selected[property] = sql.raw(cell.value).as(columnName);
|
|
2356
|
+
for (const [part, expression] of Object.entries(cell.parts)) {
|
|
2357
|
+
selected[cellColumnName(property, part)] = sql.raw(expression).as(cellColumnName(columnName, part));
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const manual = plan.plans.find((source) => source.kind === "manual");
|
|
2361
|
+
const system = RESOLVED_VIEW_SYSTEM_COLUMNS;
|
|
2362
|
+
selected[system.writebackStatus.name] = sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName)}`).as(system.writebackStatus.columnName);
|
|
2363
|
+
selected[system.writebackError.name] = sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName)}`).as(system.writebackError.columnName);
|
|
2364
|
+
const hidden = plan.deletedBy.map((name) => `NOT COALESCE(${alias(name)}.${ident(tombstoned2)}, false)`);
|
|
2365
|
+
const query = qb.select(selected).from(from);
|
|
2366
|
+
return hidden.length > 0 ? query.where(sql.raw(hidden.join(" AND "))) : query;
|
|
2367
|
+
}
|
|
2368
|
+
function managedFn(id) {
|
|
2369
|
+
return { kind: "managed", id };
|
|
2370
|
+
}
|
|
2371
|
+
function manualSource() {
|
|
2372
|
+
return { kind: "manual" };
|
|
2373
|
+
}
|
|
2374
|
+
function etlSource(input) {
|
|
2375
|
+
return { kind: "etl", ...input };
|
|
2376
|
+
}
|
|
2377
|
+
function prefetchSource(input) {
|
|
2378
|
+
return { kind: "prefetch", ...input };
|
|
2379
|
+
}
|
|
2380
|
+
var latest = (candidates) => ({ kind: "latest", candidates });
|
|
2381
|
+
var highest = (candidates) => ({ kind: "highest", candidates });
|
|
2382
|
+
var lowest = (candidates) => ({ kind: "lowest", candidates });
|
|
2383
|
+
var priority = (candidates) => ({ kind: "priority", candidates });
|
|
2384
|
+
var nonNullFirst = (candidates) => ({ kind: "nonNullFirst", candidates });
|
|
2385
|
+
var custom = (candidates, orderBy) => ({ kind: "custom", candidates, orderBy });
|
|
2386
|
+
var SYSTEM_VIEW_COLUMNS = {
|
|
2387
|
+
[RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.columnName),
|
|
2388
|
+
[RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.columnName)
|
|
2389
|
+
};
|
|
2390
|
+
function defineResolvedTable(name, columns, config) {
|
|
2391
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
2392
|
+
const fail = (message) => {
|
|
2393
|
+
throw new OrmDefinitionError(`resolved table "${name}": ${message}`, file);
|
|
2394
|
+
};
|
|
2395
|
+
if (!IDENTIFIER_PATTERN.test(name)) fail(`name must match ${IDENTIFIER_PATTERN}`);
|
|
2396
|
+
for (const property of Object.keys(columns)) {
|
|
2397
|
+
if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
|
|
2398
|
+
fail(`column "${property}" collides with a reserved system column`);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
for (const property of Object.keys(columns)) {
|
|
2402
|
+
if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
|
|
2403
|
+
fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
|
|
2407
|
+
if (pkEntries.length !== 1) fail("declare exactly one .primaryKey() column");
|
|
2408
|
+
const [pk, pkBuilder] = pkEntries[0];
|
|
2409
|
+
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2410
|
+
if (dataColumns.length === 0) fail("declare at least one non-key column");
|
|
2411
|
+
const sourceEntries = Object.entries(config.sources);
|
|
2412
|
+
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
|
|
2413
|
+
const plans = sourceEntries.map(
|
|
2414
|
+
([sourceName, input]) => planSource({ viewName: name, sourceName, input, columns, pk, pkBuilder, dataColumns, fail })
|
|
2415
|
+
);
|
|
2416
|
+
const rules = planRules({ config, plans, dataColumns, fail });
|
|
2417
|
+
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2418
|
+
for (const source of deletedBy) {
|
|
2419
|
+
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
|
|
2420
|
+
}
|
|
2421
|
+
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2422
|
+
const viewColumns = {};
|
|
2423
|
+
for (const [property, builder] of Object.entries(columns)) {
|
|
2424
|
+
viewColumns[property] = cloneColumn({ builder, property, notNull: property === pk, primaryKey: property === pk });
|
|
2425
|
+
if (property === pk) continue;
|
|
2426
|
+
const columnName = builder.columnNameFor(property);
|
|
2427
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
2428
|
+
viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
for (const [property, make] of Object.entries(SYSTEM_VIEW_COLUMNS)) viewColumns[property] = make();
|
|
2432
|
+
const { read, ...writePolicies } = config.policies;
|
|
2433
|
+
const resolved = {
|
|
2434
|
+
internalTables: plans.filter((plan) => plan.kind !== "etl").map((plan) => plan.table),
|
|
2435
|
+
sources: plans,
|
|
2436
|
+
rules,
|
|
2437
|
+
deletedBy,
|
|
2438
|
+
policies: writePolicies
|
|
2439
|
+
};
|
|
2440
|
+
const queryPlan = { columns, pk, plans, rules, deletedBy };
|
|
2441
|
+
const typedColumns = viewColumns;
|
|
2442
|
+
const surface = {
|
|
2443
|
+
...config.description !== void 0 ? { description: config.description } : {},
|
|
2444
|
+
...config.links !== void 0 ? { links: config.links } : {},
|
|
2445
|
+
policies: { read }
|
|
2446
|
+
};
|
|
2447
|
+
if (!config.materialize) {
|
|
2448
|
+
return defineView(name, typedColumns, { ...surface, query: (context) => buildResolvedQuery({ context, plan: queryPlan }), resolved });
|
|
2449
|
+
}
|
|
2450
|
+
const materialize = config.materialize === true ? {} : config.materialize;
|
|
2451
|
+
const computeName = `${name}__compute`;
|
|
2452
|
+
const tableName = `${name}__materialized`;
|
|
2453
|
+
const compute = defineView(computeName, typedColumns, {
|
|
2454
|
+
policies: { read },
|
|
2455
|
+
query: (context) => buildResolvedQuery({ context, plan: queryPlan }),
|
|
2456
|
+
internal: true
|
|
2457
|
+
});
|
|
2458
|
+
const table = defineTable(tableName, viewColumns, {
|
|
2459
|
+
description: `Materialized rows of resolved table "${name}". System-maintained from "${computeName}".`,
|
|
2460
|
+
internal: true,
|
|
2461
|
+
materializes: name,
|
|
2462
|
+
...materialize.indexes !== void 0 ? { indexes: materialize.indexes } : {}
|
|
2463
|
+
});
|
|
2464
|
+
return defineView(name, typedColumns, {
|
|
2465
|
+
...surface,
|
|
2466
|
+
query: (context) => {
|
|
2467
|
+
const handle = context.relation(table);
|
|
2468
|
+
const selected = Object.fromEntries(Object.entries(viewColumns).map(([property, builder]) => [
|
|
2469
|
+
property,
|
|
2470
|
+
sql`${handle[property]}`.as(builder.columnNameFor(property))
|
|
2471
|
+
]));
|
|
2472
|
+
return context.qb.select(selected).from(context.relation(table));
|
|
2473
|
+
},
|
|
2474
|
+
resolved: {
|
|
2475
|
+
...resolved,
|
|
2476
|
+
internalTables: [...resolved.internalTables, table],
|
|
2477
|
+
internalViews: [compute],
|
|
2478
|
+
materialized: { table: tableName, compute: computeName }
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
function planRules({ config, plans, dataColumns, fail }) {
|
|
2483
|
+
const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
|
|
2484
|
+
const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
|
|
2485
|
+
for (const column of Object.keys(config.resolve ?? {})) {
|
|
2486
|
+
if (!dataColumns.includes(column)) fail(`resolve names unknown column "${column}"`);
|
|
2487
|
+
}
|
|
2488
|
+
return dataColumns.map((column) => {
|
|
2489
|
+
const explicit = config.resolve?.[column];
|
|
2490
|
+
const comparator = explicit ?? fallback;
|
|
2491
|
+
const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name) => plans.find((plan) => plan.name === name)?.columns.includes(column));
|
|
2492
|
+
for (const candidate of candidates) {
|
|
2493
|
+
const plan = plans.find((entry) => entry.name === candidate);
|
|
2494
|
+
if (!plan) fail(`column "${column}" names unknown source "${candidate}"`);
|
|
2495
|
+
else if (!plan.columns.includes(column)) fail(`column "${column}" candidate "${candidate}" does not supply it`);
|
|
2496
|
+
}
|
|
2497
|
+
if (candidates.length === 0) fail(`column "${column}" has no candidate source`);
|
|
2498
|
+
if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail });
|
|
2499
|
+
return {
|
|
2500
|
+
column,
|
|
2501
|
+
comparator: comparator.kind,
|
|
2502
|
+
candidates,
|
|
2503
|
+
...comparator.kind === "custom" && comparator.orderBy !== void 0 ? { orderBy: comparator.orderBy } : {}
|
|
2504
|
+
};
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2508
|
+
"value",
|
|
2509
|
+
"updated_at",
|
|
2510
|
+
"source",
|
|
2511
|
+
"actor",
|
|
2512
|
+
"ord",
|
|
2513
|
+
"asc",
|
|
2514
|
+
"desc",
|
|
2515
|
+
"nulls",
|
|
2516
|
+
"first",
|
|
2517
|
+
"last",
|
|
2518
|
+
"case",
|
|
2519
|
+
"when",
|
|
2520
|
+
"then",
|
|
2521
|
+
"else",
|
|
2522
|
+
"end",
|
|
2523
|
+
"and",
|
|
2524
|
+
"or",
|
|
2525
|
+
"not",
|
|
2526
|
+
"is",
|
|
2527
|
+
"null",
|
|
2528
|
+
"true",
|
|
2529
|
+
"false",
|
|
2530
|
+
"in",
|
|
2531
|
+
"distinct",
|
|
2532
|
+
"from",
|
|
2533
|
+
"coalesce",
|
|
2534
|
+
"nullif",
|
|
2535
|
+
"greatest",
|
|
2536
|
+
"least",
|
|
2537
|
+
"text",
|
|
2538
|
+
"numeric",
|
|
2539
|
+
"integer",
|
|
2540
|
+
"bigint",
|
|
2541
|
+
"boolean",
|
|
2542
|
+
"timestamptz",
|
|
2543
|
+
"date",
|
|
2544
|
+
"jsonb",
|
|
2545
|
+
"interval"
|
|
2546
|
+
]);
|
|
2547
|
+
var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
2548
|
+
var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
|
|
2549
|
+
function assertSafeOrderBy({ orderBy, column, fail }) {
|
|
2550
|
+
ORDER_BY_TOKEN.lastIndex = 0;
|
|
2551
|
+
let consumed = 0;
|
|
2552
|
+
for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
|
|
2553
|
+
consumed = ORDER_BY_TOKEN.lastIndex;
|
|
2554
|
+
const token = match[0].toLowerCase();
|
|
2555
|
+
if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
|
|
2556
|
+
fail(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (consumed !== orderBy.length || orderBy.trim() === "") {
|
|
2560
|
+
fail(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
1389
2563
|
function stableJson(value) {
|
|
1390
2564
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1391
2565
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -1492,8 +2666,19 @@ function defineNamespace(input) {
|
|
|
1492
2666
|
return config;
|
|
1493
2667
|
}
|
|
1494
2668
|
function serializeNamespace(input) {
|
|
1495
|
-
const { config
|
|
1496
|
-
const views = input.views ?? [];
|
|
2669
|
+
const { config } = input;
|
|
2670
|
+
const views = [...input.views ?? []];
|
|
2671
|
+
const tables = [...input.tables];
|
|
2672
|
+
for (const view of views) {
|
|
2673
|
+
for (const internal of view.config.resolved?.internalTables ?? []) {
|
|
2674
|
+
if (!tables.includes(internal)) tables.push(internal);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
for (const view of [...views]) {
|
|
2678
|
+
for (const internal of view.config.resolved?.internalViews ?? []) {
|
|
2679
|
+
if (!views.includes(internal)) views.push(internal);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
1497
2682
|
const enums = input.enums ?? [];
|
|
1498
2683
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
1499
2684
|
for (const table of tables) {
|
|
@@ -1595,6 +2780,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1595
2780
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
1596
2781
|
if (table.source !== void 0) candidate.source = table.source;
|
|
1597
2782
|
if (table.source !== void 0) {
|
|
2783
|
+
if (table.config.checks?.length) {
|
|
2784
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
|
|
2785
|
+
}
|
|
1598
2786
|
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
1599
2787
|
throw new OrmDefinitionError(
|
|
1600
2788
|
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
@@ -1632,6 +2820,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1632
2820
|
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1633
2821
|
candidate.columns?.push(column);
|
|
1634
2822
|
}
|
|
2823
|
+
if (table.config.internal === true) candidate.internal = true;
|
|
2824
|
+
if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
|
|
2825
|
+
if (table.config.cellMetadata === true && table.source !== void 0) {
|
|
2826
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
|
|
2827
|
+
}
|
|
1635
2828
|
if (table.config.primaryKey !== void 0) {
|
|
1636
2829
|
if (columnLevelPk.length > 0) {
|
|
1637
2830
|
throw new OrmDefinitionError(
|
|
@@ -1645,6 +2838,45 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1645
2838
|
} else if (columnLevelPk.length > 0) {
|
|
1646
2839
|
candidate.primaryKey = columnLevelPk;
|
|
1647
2840
|
}
|
|
2841
|
+
if (table.config.cellMetadata === true) {
|
|
2842
|
+
candidate.cellMetadata = true;
|
|
2843
|
+
const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
|
|
2844
|
+
const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
|
|
2845
|
+
for (const column of tracked) {
|
|
2846
|
+
for (const part of parts) {
|
|
2847
|
+
candidate.columns?.push({
|
|
2848
|
+
name: cellColumnName(column.name, part),
|
|
2849
|
+
columnName: cellColumnName(column.columnName, part),
|
|
2850
|
+
type: CELL_PART_TYPES[part],
|
|
2851
|
+
nullable: true,
|
|
2852
|
+
unique: false,
|
|
2853
|
+
deprecated: false,
|
|
2854
|
+
cell: { of: column.name, part }
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
if (table.config.foreignKeys?.length) {
|
|
2860
|
+
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
2861
|
+
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
2862
|
+
const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
|
|
2863
|
+
throw new OrmDefinitionError(message, table.file);
|
|
2864
|
+
} });
|
|
2865
|
+
if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
|
|
2866
|
+
return {
|
|
2867
|
+
name: fk.name,
|
|
2868
|
+
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
2869
|
+
target: target.tableName,
|
|
2870
|
+
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
2871
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {},
|
|
2872
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
2873
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
2874
|
+
};
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
if (table.config.checks?.length) {
|
|
2878
|
+
candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
|
|
2879
|
+
}
|
|
1648
2880
|
for (const index3 of table.config.indexes ?? []) {
|
|
1649
2881
|
const entry = {
|
|
1650
2882
|
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1697,6 +2929,12 @@ function buildCanonicalView({
|
|
|
1697
2929
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1698
2930
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
|
|
1699
2931
|
}
|
|
2932
|
+
if (column.onUpdateFn !== void 0) {
|
|
2933
|
+
throw new OrmDefinitionError(
|
|
2934
|
+
`view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
|
|
2935
|
+
view.file
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
1700
2938
|
if (column.unique) {
|
|
1701
2939
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
|
|
1702
2940
|
}
|
|
@@ -1734,7 +2972,33 @@ function buildCanonicalView({
|
|
|
1734
2972
|
columns,
|
|
1735
2973
|
...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
|
|
1736
2974
|
links,
|
|
1737
|
-
policies: { read: toCanonicalPolicy(view.config.policies.read) }
|
|
2975
|
+
policies: { read: toCanonicalPolicy(view.config.policies.read) },
|
|
2976
|
+
...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
|
|
2977
|
+
...view.config.internal === true ? { internal: true } : {}
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
function buildCanonicalResolved(resolved) {
|
|
2981
|
+
const policies = {};
|
|
2982
|
+
if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
|
|
2983
|
+
if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
|
|
2984
|
+
if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
|
|
2985
|
+
return {
|
|
2986
|
+
sources: resolved.sources.map((source) => ({
|
|
2987
|
+
name: source.name,
|
|
2988
|
+
kind: source.kind,
|
|
2989
|
+
table: source.table.tableName,
|
|
2990
|
+
columns: [...source.columns],
|
|
2991
|
+
...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
|
|
2992
|
+
...source.key !== void 0 ? { key: source.key } : {},
|
|
2993
|
+
...source.fetch !== void 0 ? { fetch: source.fetch } : {},
|
|
2994
|
+
...source.function !== void 0 ? { function: { ...source.function } } : {},
|
|
2995
|
+
...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
|
|
2996
|
+
...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
|
|
2997
|
+
})),
|
|
2998
|
+
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
2999
|
+
deletedBy: [...resolved.deletedBy],
|
|
3000
|
+
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
3001
|
+
policies
|
|
1738
3002
|
};
|
|
1739
3003
|
}
|
|
1740
3004
|
function buildCanonicalLink({
|
|
@@ -1751,6 +3015,9 @@ function buildCanonicalLink({
|
|
|
1751
3015
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
1752
3016
|
return fail("sets enforce: true, which is only supported on toOne links");
|
|
1753
3017
|
}
|
|
3018
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
|
|
3019
|
+
return fail("sets a referential action without enforce: true");
|
|
3020
|
+
}
|
|
1754
3021
|
if (link.linkKind === "toPlatformUser") {
|
|
1755
3022
|
if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
|
|
1756
3023
|
return {
|
|
@@ -1779,6 +3046,8 @@ function buildCanonicalLink({
|
|
|
1779
3046
|
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1780
3047
|
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
1781
3048
|
enforce: link.enforce,
|
|
3049
|
+
...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
|
|
3050
|
+
...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
|
|
1782
3051
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1783
3052
|
};
|
|
1784
3053
|
}
|
|
@@ -1957,6 +3226,10 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
1957
3226
|
fsCache: false,
|
|
1958
3227
|
...options.ormModulePath !== void 0 ? {
|
|
1959
3228
|
alias: {
|
|
3229
|
+
// The public /drizzle barrel re-exports this pinned dependency.
|
|
3230
|
+
// Explicitly map it before the root alias: otherwise jiti appends
|
|
3231
|
+
// "/drizzle" to the overridden module's filename.
|
|
3232
|
+
"@sequenceholdings/orm/drizzle": createRequire(import.meta.url).resolve("drizzle-orm"),
|
|
1960
3233
|
"@sequenceholdings/orm": options.ormModulePath,
|
|
1961
3234
|
"@sequenceholdings/orm/authoring": options.ormModulePath
|
|
1962
3235
|
}
|
|
@@ -2228,7 +3501,7 @@ var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
|
|
|
2228
3501
|
var IDENTIFIER_START = /[a-z_]/;
|
|
2229
3502
|
var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
|
|
2230
3503
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
2231
|
-
function
|
|
3504
|
+
function tokenize2(source) {
|
|
2232
3505
|
const tokens = [];
|
|
2233
3506
|
let position = 0;
|
|
2234
3507
|
while (position < source.length) {
|
|
@@ -2302,7 +3575,7 @@ function readString(source, start) {
|
|
|
2302
3575
|
}
|
|
2303
3576
|
throw new PredicateParseError("unterminated string literal", start);
|
|
2304
3577
|
}
|
|
2305
|
-
var
|
|
3578
|
+
var Parser2 = class {
|
|
2306
3579
|
constructor(tokens) {
|
|
2307
3580
|
this.tokens = tokens;
|
|
2308
3581
|
}
|
|
@@ -2434,7 +3707,7 @@ var Parser = class {
|
|
|
2434
3707
|
function parsePredicate(source) {
|
|
2435
3708
|
const trimmed = source.trim();
|
|
2436
3709
|
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
2437
|
-
return { source: trimmed, ast: new
|
|
3710
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
2438
3711
|
}
|
|
2439
3712
|
|
|
2440
3713
|
// src/schema/predicates.ts
|
|
@@ -2519,8 +3792,8 @@ var actionSchema = z.strictObject({
|
|
|
2519
3792
|
}
|
|
2520
3793
|
}
|
|
2521
3794
|
});
|
|
2522
|
-
function extractNamedParams(
|
|
2523
|
-
const withoutStrings =
|
|
3795
|
+
function extractNamedParams(sql7) {
|
|
3796
|
+
const withoutStrings = sql7.replace(QUOTED_STRING_PATTERN, "''");
|
|
2524
3797
|
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
2525
3798
|
return [...new Set(names)];
|
|
2526
3799
|
}
|
|
@@ -2587,17 +3860,6 @@ var SQL_TYPE_NAMES = {
|
|
|
2587
3860
|
};
|
|
2588
3861
|
var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
|
|
2589
3862
|
|
|
2590
|
-
// src/compiler/sql.ts
|
|
2591
|
-
var CLAIMS_SETTING = "request.jwt.claims";
|
|
2592
|
-
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
2593
|
-
var ROLES_CLAIM_KEY = "roles";
|
|
2594
|
-
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
2595
|
-
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
2596
|
-
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
2597
|
-
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
2598
|
-
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
2599
|
-
var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
2600
|
-
|
|
2601
3863
|
// src/compiler/predicates.ts
|
|
2602
3864
|
var SQL_COMPARE_OPS = {
|
|
2603
3865
|
"=": "=",
|
|
@@ -2677,8 +3939,8 @@ var actionParamArg = (name) => `p_${name}`;
|
|
|
2677
3939
|
function compileActionStatements(action, renderParam = actionParamArg) {
|
|
2678
3940
|
return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
|
|
2679
3941
|
}
|
|
2680
|
-
function compileStatement(
|
|
2681
|
-
const compiled =
|
|
3942
|
+
function compileStatement(sql7, renderParam) {
|
|
3943
|
+
const compiled = sql7.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
|
|
2682
3944
|
if (claimKey !== void 0) {
|
|
2683
3945
|
if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
|
|
2684
3946
|
return claimExpression(claimKey);
|
|
@@ -2743,10 +4005,10 @@ function physicalColumn(logical, options) {
|
|
|
2743
4005
|
return options.columnNames?.[logical] ?? logical;
|
|
2744
4006
|
}
|
|
2745
4007
|
function compileCmp(op, column, value, options) {
|
|
2746
|
-
const
|
|
2747
|
-
const declared = options.columnTypes?.[
|
|
4008
|
+
const physical2 = physicalColumn(column, options);
|
|
4009
|
+
const declared = options.columnTypes?.[physical2] ?? options.columnTypes?.[column];
|
|
2748
4010
|
const isEnum = typeof declared === "object" && declared !== null;
|
|
2749
|
-
const col = isEnum ? `${quoteIdentifier(
|
|
4011
|
+
const col = isEnum ? `${quoteIdentifier(physical2)}::text` : quoteIdentifier(physical2);
|
|
2750
4012
|
const columnType = isEnum ? "text" : declared;
|
|
2751
4013
|
switch (op) {
|
|
2752
4014
|
case "eq":
|
|
@@ -2790,11 +4052,11 @@ function compileEquality(col, value, columnType, negated, options) {
|
|
|
2790
4052
|
function compileValue(value, columnType, _options) {
|
|
2791
4053
|
switch (value.kind) {
|
|
2792
4054
|
case "literal": {
|
|
2793
|
-
const
|
|
2794
|
-
if (
|
|
2795
|
-
if (typeof
|
|
2796
|
-
if (typeof
|
|
2797
|
-
return String(
|
|
4055
|
+
const literal2 = value.value;
|
|
4056
|
+
if (literal2 === null) return "NULL";
|
|
4057
|
+
if (typeof literal2 === "string") return quoteLiteral(literal2);
|
|
4058
|
+
if (typeof literal2 === "boolean") return literal2 ? "TRUE" : "FALSE";
|
|
4059
|
+
return String(literal2);
|
|
2798
4060
|
}
|
|
2799
4061
|
case "claim": {
|
|
2800
4062
|
const expression = claimExpression(value.key);
|
|
@@ -2843,8 +4105,8 @@ function actionPrepareChecks(definition) {
|
|
|
2843
4105
|
const order = actionParamOrder(action);
|
|
2844
4106
|
const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
|
|
2845
4107
|
const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
|
|
2846
|
-
compiled.forEach((
|
|
2847
|
-
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns,
|
|
4108
|
+
compiled.forEach((sql7, index3) => {
|
|
4109
|
+
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql7) : sql7;
|
|
2848
4110
|
statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
|
|
2849
4111
|
});
|
|
2850
4112
|
}
|
|
@@ -3226,14 +4488,14 @@ function comparisonTypeIssue(left, right, columns) {
|
|
|
3226
4488
|
return null;
|
|
3227
4489
|
}
|
|
3228
4490
|
const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
|
|
3229
|
-
const
|
|
3230
|
-
if (!column || !
|
|
4491
|
+
const literal2 = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
|
|
4492
|
+
if (!column || !literal2 || literal2.value === null) return null;
|
|
3231
4493
|
const columnGroup = groupOf(column);
|
|
3232
4494
|
if (!columnGroup) return null;
|
|
3233
|
-
if (typeof
|
|
3234
|
-
const literalGroup = typeof
|
|
4495
|
+
if (typeof literal2.value === "string") return null;
|
|
4496
|
+
const literalGroup = typeof literal2.value === "number" ? "numeric" : "boolean";
|
|
3235
4497
|
if (literalGroup !== columnGroup) {
|
|
3236
|
-
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof
|
|
4498
|
+
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal2.value} literal \u2014 incomparable types`;
|
|
3237
4499
|
}
|
|
3238
4500
|
return null;
|
|
3239
4501
|
}
|
|
@@ -3298,7 +4560,8 @@ function syncedViewStatements({
|
|
|
3298
4560
|
readPolicyIrByTable,
|
|
3299
4561
|
columnNamesByTable,
|
|
3300
4562
|
columnTypesByTable,
|
|
3301
|
-
executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
|
|
4563
|
+
executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE,
|
|
4564
|
+
backingPrivileges = "grant"
|
|
3302
4565
|
}) {
|
|
3303
4566
|
const schema = quoteIdentifier(ormSchemaName(definition.namespace));
|
|
3304
4567
|
const role = quoteIdentifier(consumerRoleName(definition.namespace));
|
|
@@ -3318,8 +4581,8 @@ function syncedViewStatements({
|
|
|
3318
4581
|
const backing = backingMap.get(table.table);
|
|
3319
4582
|
if (!backing) continue;
|
|
3320
4583
|
const viewName = `${schema}.${quoteIdentifier(table.table)}`;
|
|
3321
|
-
const
|
|
3322
|
-
const physicalTarget = `${quoteIdentifier(
|
|
4584
|
+
const physical2 = splitPhysicalRelation(backing.physicalRelation);
|
|
4585
|
+
const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
|
|
3323
4586
|
const columnTypes = mergeSyncedViewColumnTypes({
|
|
3324
4587
|
table,
|
|
3325
4588
|
canonicalColumnTypes: columnTypesByTable?.get(table.table),
|
|
@@ -3332,8 +4595,10 @@ function syncedViewStatements({
|
|
|
3332
4595
|
columnTypes,
|
|
3333
4596
|
columnNames: columnNamesByTable?.get(table.table)
|
|
3334
4597
|
})}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
|
|
3335
|
-
|
|
3336
|
-
|
|
4598
|
+
if (backingPrivileges === "grant") {
|
|
4599
|
+
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical2.schema)} TO ${owner}`);
|
|
4600
|
+
statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
|
|
4601
|
+
}
|
|
3337
4602
|
statements.push(
|
|
3338
4603
|
`CREATE OR REPLACE VIEW ${viewName} AS SELECT * FROM ${physicalTarget}${whereClause}`
|
|
3339
4604
|
);
|
|
@@ -3352,10 +4617,10 @@ function mergeSyncedViewColumnTypes({
|
|
|
3352
4617
|
if (def?.type) fromTable[name] = def.type;
|
|
3353
4618
|
}
|
|
3354
4619
|
if (columnNames) {
|
|
3355
|
-
for (const [logical,
|
|
3356
|
-
const declared = fromTable[
|
|
4620
|
+
for (const [logical, physical2] of Object.entries(columnNames)) {
|
|
4621
|
+
const declared = fromTable[physical2] ?? fromTable[logical];
|
|
3357
4622
|
if (!declared) continue;
|
|
3358
|
-
fromTable[
|
|
4623
|
+
fromTable[physical2] = declared;
|
|
3359
4624
|
fromTable[logical] = declared;
|
|
3360
4625
|
}
|
|
3361
4626
|
}
|
|
@@ -3394,8 +4659,8 @@ function syncedBackingRevokeStatements({
|
|
|
3394
4659
|
const seen = /* @__PURE__ */ new Set();
|
|
3395
4660
|
const statements = [];
|
|
3396
4661
|
for (const backing of backings) {
|
|
3397
|
-
const
|
|
3398
|
-
const physicalTarget = `${quoteIdentifier(
|
|
4662
|
+
const physical2 = splitPhysicalRelation(backing.physicalRelation);
|
|
4663
|
+
const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
|
|
3399
4664
|
if (seen.has(physicalTarget)) continue;
|
|
3400
4665
|
seen.add(physicalTarget);
|
|
3401
4666
|
statements.push(`REVOKE SELECT ON ${physicalTarget} FROM ${owner}`);
|
|
@@ -3605,6 +4870,90 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
3605
4870
|
const tables = snapshot.tables ?? {};
|
|
3606
4871
|
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
3607
4872
|
}
|
|
4873
|
+
|
|
4874
|
+
// src/engine/materialized-refresh.ts
|
|
4875
|
+
function materializedRefreshFunctions(canonical) {
|
|
4876
|
+
const schema = ormSchemaName(canonical.namespace);
|
|
4877
|
+
const functions = /* @__PURE__ */ new Map();
|
|
4878
|
+
for (const view of canonical.views ?? []) {
|
|
4879
|
+
const materialized = view.resolved?.materialized;
|
|
4880
|
+
if (!materialized) continue;
|
|
4881
|
+
const table = canonical.tables.find((candidate) => candidate.table === materialized.table);
|
|
4882
|
+
const pk = view.primaryKey ?? [];
|
|
4883
|
+
if (!table || pk.length === 0) {
|
|
4884
|
+
throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
|
|
4885
|
+
}
|
|
4886
|
+
functions.set(view.view, refreshFunctionSql({
|
|
4887
|
+
schema,
|
|
4888
|
+
name: view.view,
|
|
4889
|
+
table: materialized.table,
|
|
4890
|
+
compute: materialized.compute,
|
|
4891
|
+
columns: table.columns.map((column) => column.columnName),
|
|
4892
|
+
pk
|
|
4893
|
+
}));
|
|
4894
|
+
}
|
|
4895
|
+
return functions;
|
|
4896
|
+
}
|
|
4897
|
+
function refreshFunctionSql({ schema, name, table, compute, columns, pk }) {
|
|
4898
|
+
const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name))}`;
|
|
4899
|
+
const target = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
|
4900
|
+
const source = `${quoteIdentifier(schema)}.${quoteIdentifier(compute)}`;
|
|
4901
|
+
const list = columns.map(quoteIdentifier).join(", ");
|
|
4902
|
+
const others = columns.filter((column) => !pk.includes(column));
|
|
4903
|
+
const keys = pk.map(quoteIdentifier).join(", ");
|
|
4904
|
+
const matches = pk.map((column) => `c.${quoteIdentifier(column)} = m.${quoteIdentifier(column)}`).join(" AND ");
|
|
4905
|
+
const onConflict = others.length === 0 ? "DO NOTHING" : `DO UPDATE SET ${others.map((column) => `${quoteIdentifier(column)} = EXCLUDED.${quoteIdentifier(column)}`).join(", ")}
|
|
4906
|
+
WHERE (${others.map((column) => `m.${quoteIdentifier(column)}`).join(", ")}) IS DISTINCT FROM (${others.map((column) => `EXCLUDED.${quoteIdentifier(column)}`).join(", ")})`;
|
|
4907
|
+
const lockKey = quoteLiteral(materializeLockKey(schema, name));
|
|
4908
|
+
return `CREATE OR REPLACE FUNCTION ${fn}(wait boolean DEFAULT false)
|
|
4909
|
+
RETURNS TABLE(upserted bigint, deleted bigint, locked boolean)
|
|
4910
|
+
LANGUAGE plpgsql
|
|
4911
|
+
SECURITY INVOKER
|
|
4912
|
+
SET search_path = pg_catalog, pg_temp
|
|
4913
|
+
AS $orm_refresh$
|
|
4914
|
+
#variable_conflict use_column
|
|
4915
|
+
DECLARE
|
|
4916
|
+
caller_claims text := current_setting('request.jwt.claims', true);
|
|
4917
|
+
BEGIN
|
|
4918
|
+
IF wait THEN
|
|
4919
|
+
PERFORM pg_advisory_xact_lock(hashtext(${lockKey}));
|
|
4920
|
+
ELSIF NOT pg_try_advisory_xact_lock(hashtext(${lockKey})) THEN
|
|
4921
|
+
RETURN QUERY SELECT 0::bigint, 0::bigint, false;
|
|
4922
|
+
RETURN;
|
|
4923
|
+
END IF;
|
|
4924
|
+
PERFORM set_config('request.jwt.claims', ${quoteLiteral(MATERIALIZE_CLAIMS_JSON)}, true);
|
|
4925
|
+
RETURN QUERY
|
|
4926
|
+
WITH up AS (
|
|
4927
|
+
INSERT INTO ${target} AS m (${list})
|
|
4928
|
+
SELECT ${list} FROM ${source}
|
|
4929
|
+
ON CONFLICT (${keys}) ${onConflict}
|
|
4930
|
+
RETURNING 1
|
|
4931
|
+
), gone AS (
|
|
4932
|
+
DELETE FROM ${target} AS m
|
|
4933
|
+
WHERE NOT EXISTS (SELECT 1 FROM ${source} AS c WHERE ${matches})
|
|
4934
|
+
RETURNING 1
|
|
4935
|
+
)
|
|
4936
|
+
SELECT (SELECT count(*) FROM up), (SELECT count(*) FROM gone), true;
|
|
4937
|
+
-- NULL resets to the session value when the caller had none.
|
|
4938
|
+
PERFORM set_config('request.jwt.claims', caller_claims, true);
|
|
4939
|
+
END
|
|
4940
|
+
$orm_refresh$`;
|
|
4941
|
+
}
|
|
4942
|
+
function refreshFunctionStatements({ namespace, previous, current }) {
|
|
4943
|
+
const schema = quoteIdentifier(ormSchemaName(namespace));
|
|
4944
|
+
const statements = [];
|
|
4945
|
+
for (const name of Object.keys(previous).sort()) {
|
|
4946
|
+
if (!current.has(name)) statements.push(`DROP FUNCTION IF EXISTS ${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`);
|
|
4947
|
+
}
|
|
4948
|
+
for (const [name, create] of [...current].sort(([a], [b]) => a.localeCompare(b))) {
|
|
4949
|
+
if (previous[name] === create) continue;
|
|
4950
|
+
const fn = `${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`;
|
|
4951
|
+
statements.push(create, `REVOKE ALL ON FUNCTION ${fn} FROM PUBLIC`);
|
|
4952
|
+
}
|
|
4953
|
+
return statements;
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
// src/engine/canonical2.ts
|
|
3608
4957
|
var FACTORIES2 = {
|
|
3609
4958
|
uuid: (name) => uuid(name),
|
|
3610
4959
|
text: (name) => text(name),
|
|
@@ -3622,7 +4971,7 @@ var FACTORIES2 = {
|
|
|
3622
4971
|
var SCHEMA_IMPORT_KEY2 = "$schema";
|
|
3623
4972
|
var enumImportKey = (name) => `$enum_${name}`;
|
|
3624
4973
|
var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
|
|
3625
|
-
function buildDrizzleEntitiesV2(definition) {
|
|
4974
|
+
function buildDrizzleEntitiesV2(definition, options = {}) {
|
|
3626
4975
|
const schemaName = ormSchemaName(definition.namespace);
|
|
3627
4976
|
const schema = pgSchema(schemaName);
|
|
3628
4977
|
const imports = { [SCHEMA_IMPORT_KEY2]: schema };
|
|
@@ -3637,11 +4986,19 @@ function buildDrizzleEntitiesV2(definition) {
|
|
|
3637
4986
|
}
|
|
3638
4987
|
const byName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
3639
4988
|
for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
|
|
3640
|
-
const drizzleTable = buildTable2(schema, table, built, enums, byName);
|
|
4989
|
+
const drizzleTable = buildTable2(schema, table, built, enums, byName, options.introspection ?? false);
|
|
3641
4990
|
built.set(table.table, drizzleTable);
|
|
3642
4991
|
imports[table.table] = drizzleTable;
|
|
3643
4992
|
tables[table.table] = drizzleTable;
|
|
3644
4993
|
}
|
|
4994
|
+
if (options.introspection) {
|
|
4995
|
+
for (const table of definition.tables.filter((table2) => table2.source)) {
|
|
4996
|
+
const columns = Object.fromEntries(
|
|
4997
|
+
table.columns.map((column) => [column.columnName, buildViewColumnV2({ view: table.table, column, enums })])
|
|
4998
|
+
);
|
|
4999
|
+
imports[table.table] = schema.view(table.table, columns).existing();
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
3645
5002
|
for (const view of definition.views ?? []) {
|
|
3646
5003
|
const columns = Object.fromEntries(
|
|
3647
5004
|
view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
|
|
@@ -3709,18 +5066,18 @@ function rewriteSyncedSourceRelationsInViewQuery({
|
|
|
3709
5066
|
return rewritten;
|
|
3710
5067
|
});
|
|
3711
5068
|
}
|
|
3712
|
-
function transformSqlOutsideLiterals(
|
|
5069
|
+
function transformSqlOutsideLiterals(sql7, transform) {
|
|
3713
5070
|
const out = [];
|
|
3714
5071
|
let state = { kind: "normal" };
|
|
3715
5072
|
let i = 0;
|
|
3716
5073
|
let codeStart = 0;
|
|
3717
5074
|
const emitCode = (end) => {
|
|
3718
|
-
if (end > codeStart) out.push(transform(
|
|
5075
|
+
if (end > codeStart) out.push(transform(sql7.slice(codeStart, end)));
|
|
3719
5076
|
codeStart = end;
|
|
3720
5077
|
};
|
|
3721
|
-
while (i <
|
|
3722
|
-
const c =
|
|
3723
|
-
const next = i + 1 <
|
|
5078
|
+
while (i < sql7.length) {
|
|
5079
|
+
const c = sql7[i];
|
|
5080
|
+
const next = i + 1 < sql7.length ? sql7[i + 1] : "";
|
|
3724
5081
|
switch (state.kind) {
|
|
3725
5082
|
case "normal": {
|
|
3726
5083
|
if (c === "-" && next === "-") {
|
|
@@ -3740,7 +5097,7 @@ function transformSqlOutsideLiterals(sql4, transform) {
|
|
|
3740
5097
|
break;
|
|
3741
5098
|
}
|
|
3742
5099
|
if ((c === "E" || c === "e") && next === "'") {
|
|
3743
|
-
const prev = i > 0 ?
|
|
5100
|
+
const prev = i > 0 ? sql7[i - 1] : "";
|
|
3744
5101
|
if (!/[A-Za-z0-9_]/.test(prev)) {
|
|
3745
5102
|
emitCode(i);
|
|
3746
5103
|
state = { kind: "e-single" };
|
|
@@ -3759,9 +5116,9 @@ function transformSqlOutsideLiterals(sql4, transform) {
|
|
|
3759
5116
|
break;
|
|
3760
5117
|
}
|
|
3761
5118
|
if (c === "$") {
|
|
3762
|
-
const prev = i > 0 ?
|
|
5119
|
+
const prev = i > 0 ? sql7[i - 1] : "";
|
|
3763
5120
|
if (!/[A-Za-z0-9_$]/.test(prev)) {
|
|
3764
|
-
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(
|
|
5121
|
+
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql7.slice(i));
|
|
3765
5122
|
if (tagMatch) {
|
|
3766
5123
|
emitCode(i);
|
|
3767
5124
|
state = { kind: "dollar", tag: tagMatch[1] ?? "" };
|
|
@@ -3793,8 +5150,8 @@ function transformSqlOutsideLiterals(sql4, transform) {
|
|
|
3793
5150
|
break;
|
|
3794
5151
|
}
|
|
3795
5152
|
case "e-single": {
|
|
3796
|
-
if (c === "\\" && i + 1 <
|
|
3797
|
-
out.push(c,
|
|
5153
|
+
if (c === "\\" && i + 1 < sql7.length) {
|
|
5154
|
+
out.push(c, sql7[i + 1]);
|
|
3798
5155
|
i += 2;
|
|
3799
5156
|
break;
|
|
3800
5157
|
}
|
|
@@ -3816,7 +5173,7 @@ function transformSqlOutsideLiterals(sql4, transform) {
|
|
|
3816
5173
|
}
|
|
3817
5174
|
case "dollar": {
|
|
3818
5175
|
const closer = `$${state.tag}$`;
|
|
3819
|
-
if (
|
|
5176
|
+
if (sql7.startsWith(closer, i)) {
|
|
3820
5177
|
out.push(closer);
|
|
3821
5178
|
state = { kind: "normal" };
|
|
3822
5179
|
i += closer.length;
|
|
@@ -3860,15 +5217,15 @@ function transformSqlOutsideLiterals(sql4, transform) {
|
|
|
3860
5217
|
}
|
|
3861
5218
|
}
|
|
3862
5219
|
}
|
|
3863
|
-
emitCode(
|
|
5220
|
+
emitCode(sql7.length);
|
|
3864
5221
|
return out.join("");
|
|
3865
5222
|
}
|
|
3866
5223
|
function escapeRegExp(value) {
|
|
3867
5224
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3868
5225
|
}
|
|
3869
|
-
function sqlReferencesQualifiedRelation(
|
|
5226
|
+
function sqlReferencesQualifiedRelation(sql7, sourceSchema, sourceName) {
|
|
3870
5227
|
let referenced = false;
|
|
3871
|
-
transformSqlOutsideLiterals(
|
|
5228
|
+
transformSqlOutsideLiterals(sql7, (segment) => {
|
|
3872
5229
|
if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
|
|
3873
5230
|
return segment;
|
|
3874
5231
|
});
|
|
@@ -3879,15 +5236,18 @@ function qualifiedRelationPattern(sourceSchema, sourceName) {
|
|
|
3879
5236
|
const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
|
|
3880
5237
|
return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
|
|
3881
5238
|
}
|
|
3882
|
-
function replaceQualifiedRelation(
|
|
3883
|
-
return
|
|
5239
|
+
function replaceQualifiedRelation(sql7, sourceSchema, sourceName, boundSchema, boundTable) {
|
|
5240
|
+
return sql7.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
|
|
3884
5241
|
}
|
|
3885
5242
|
function generateSnapshotV2(definition, previousId3) {
|
|
3886
5243
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
3887
5244
|
const snapshot = generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
3888
|
-
if ((definition.views?.length ?? 0) > 0) {
|
|
5245
|
+
if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
|
|
3889
5246
|
const extended = snapshot;
|
|
5247
|
+
const refreshFunctions = materializedRefreshFunctions(definition);
|
|
3890
5248
|
extended._sequenceOrm = {
|
|
5249
|
+
...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
|
|
5250
|
+
...refreshFunctions.size > 0 ? { refreshFunctions: Object.fromEntries(refreshFunctions) } : {},
|
|
3891
5251
|
viewDependencies: Object.fromEntries(
|
|
3892
5252
|
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
3893
5253
|
)
|
|
@@ -3905,7 +5265,7 @@ function buildViewColumnV2({
|
|
|
3905
5265
|
if (!column.nullable) builder = builder.notNull();
|
|
3906
5266
|
return builder;
|
|
3907
5267
|
}
|
|
3908
|
-
function buildTable2(schema, table, built, enums, byName) {
|
|
5268
|
+
function buildTable2(schema, table, built, enums, byName, introspection) {
|
|
3909
5269
|
const primaryKeyColumns = table.primaryKey ?? [];
|
|
3910
5270
|
if (primaryKeyColumns.length === 0) {
|
|
3911
5271
|
throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
|
|
@@ -3921,13 +5281,26 @@ function buildTable2(schema, table, built, enums, byName) {
|
|
|
3921
5281
|
const columns = Object.fromEntries(
|
|
3922
5282
|
table.columns.map((column) => [
|
|
3923
5283
|
column.columnName,
|
|
3924
|
-
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
|
|
5284
|
+
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums, introspection })
|
|
3925
5285
|
])
|
|
3926
5286
|
);
|
|
3927
5287
|
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
3928
5288
|
...buildCompositePrimaryKey(table, primaryKeyColumns, self),
|
|
3929
5289
|
...buildIndexes2(table, self),
|
|
3930
|
-
...
|
|
5290
|
+
...(table.checks ?? []).map((check$1) => check(check$1.name, sql.raw(compileCheckExpression(check$1.expression, table.columns)))),
|
|
5291
|
+
...buildForeignKeys2(table, self, built, byName),
|
|
5292
|
+
...(table.foreignKeys ?? []).map((fk) => {
|
|
5293
|
+
const target = fk.target === table.table ? self : built.get(fk.target);
|
|
5294
|
+
if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
|
|
5295
|
+
let constraint = foreignKey({
|
|
5296
|
+
name: fk.name,
|
|
5297
|
+
columns: fk.columns.map((column) => self[column]),
|
|
5298
|
+
foreignColumns: fk.references.map((column) => target[column])
|
|
5299
|
+
});
|
|
5300
|
+
if (fk.onDelete !== void 0) constraint = constraint.onDelete(fk.onDelete);
|
|
5301
|
+
if (fk.onUpdate !== void 0) constraint = constraint.onUpdate(fk.onUpdate);
|
|
5302
|
+
return constraint;
|
|
5303
|
+
})
|
|
3931
5304
|
]);
|
|
3932
5305
|
return drizzleTable.enableRLS();
|
|
3933
5306
|
}
|
|
@@ -3944,7 +5317,8 @@ function buildColumnV2({
|
|
|
3944
5317
|
column,
|
|
3945
5318
|
singlePrimaryKey,
|
|
3946
5319
|
compositeMembers,
|
|
3947
|
-
enums
|
|
5320
|
+
enums,
|
|
5321
|
+
introspection
|
|
3948
5322
|
}) {
|
|
3949
5323
|
const factory = resolveColumnFactory(table, column, enums);
|
|
3950
5324
|
let builder = factory(column.columnName);
|
|
@@ -3965,7 +5339,7 @@ function buildColumnV2({
|
|
|
3965
5339
|
}
|
|
3966
5340
|
builder = builder.default(sql.raw(column.defaultFn));
|
|
3967
5341
|
} else if (column.default !== void 0 && column.default !== null) {
|
|
3968
|
-
builder = builder.default(column.default);
|
|
5342
|
+
builder = introspection && column.type === "jsonb" && typeof column.default === "string" ? builder.default(sql.raw(`${quoteLiteral(column.default)}::jsonb`)) : builder.default(column.default);
|
|
3969
5343
|
}
|
|
3970
5344
|
return builder;
|
|
3971
5345
|
}
|
|
@@ -4016,11 +5390,14 @@ function buildForeignKeys2(table, self, built, byName) {
|
|
|
4016
5390
|
`enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
|
|
4017
5391
|
);
|
|
4018
5392
|
}
|
|
4019
|
-
|
|
5393
|
+
let constraint = foreignKey({
|
|
4020
5394
|
name: foreignKeyName(table.table, link.name),
|
|
4021
5395
|
columns: [sourceColumn],
|
|
4022
5396
|
foreignColumns: [targetColumn]
|
|
4023
5397
|
});
|
|
5398
|
+
if (link.onDelete !== void 0) constraint = constraint.onDelete(link.onDelete);
|
|
5399
|
+
if (link.onUpdate !== void 0) constraint = constraint.onUpdate(link.onUpdate);
|
|
5400
|
+
return constraint;
|
|
4024
5401
|
});
|
|
4025
5402
|
}
|
|
4026
5403
|
var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
|
|
@@ -4141,16 +5518,31 @@ function findTable2(snapshot, schema, table) {
|
|
|
4141
5518
|
const tables = snapshot.tables ?? {};
|
|
4142
5519
|
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
4143
5520
|
}
|
|
4144
|
-
|
|
5521
|
+
function planMigrationV2(input) {
|
|
5522
|
+
return planWithViewRecreation({ ...input, viewRecreation: "affected" });
|
|
5523
|
+
}
|
|
5524
|
+
async function planCommittedMigrationV2({
|
|
4145
5525
|
canonical,
|
|
4146
|
-
previousSnapshot
|
|
5526
|
+
previousSnapshot,
|
|
5527
|
+
reproduces
|
|
5528
|
+
}) {
|
|
5529
|
+
const current = await planMigrationV2({ canonical, previousSnapshot });
|
|
5530
|
+
if (reproduces(current)) return { plan: current, reproduced: true };
|
|
5531
|
+
const legacy = await planWithViewRecreation({ canonical, previousSnapshot, viewRecreation: "all-on-base-change" });
|
|
5532
|
+
return reproduces(legacy) ? { plan: legacy, reproduced: true } : { plan: current, reproduced: false };
|
|
5533
|
+
}
|
|
5534
|
+
async function planWithViewRecreation({
|
|
5535
|
+
canonical,
|
|
5536
|
+
previousSnapshot,
|
|
5537
|
+
viewRecreation
|
|
4147
5538
|
}) {
|
|
4148
5539
|
const schema = ormSchemaName(canonical.namespace);
|
|
4149
5540
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
4150
5541
|
assertValidViewDependencies(canonical);
|
|
4151
5542
|
const hints = applyMigrationHintsV2(previous, canonical);
|
|
4152
5543
|
const snapshot = generateSnapshotV2(canonical, previousId(previous));
|
|
4153
|
-
const
|
|
5544
|
+
const generatedBaseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
|
|
5545
|
+
const baseDdl = orderForeignKeyAdditions(canonical, generatedBaseDdl);
|
|
4154
5546
|
const previousViews = snapshotViews(hints.adjusted);
|
|
4155
5547
|
const currentViews = snapshotViews(snapshot);
|
|
4156
5548
|
const previousDependencies = snapshotViewDependencies(hints.adjusted);
|
|
@@ -4160,7 +5552,14 @@ async function planMigrationV2({
|
|
|
4160
5552
|
currentViews,
|
|
4161
5553
|
previousDependencies,
|
|
4162
5554
|
currentDependencies,
|
|
4163
|
-
|
|
5555
|
+
reach: viewReach({
|
|
5556
|
+
viewRecreation,
|
|
5557
|
+
hintStatements: hints.statements,
|
|
5558
|
+
baseDdl,
|
|
5559
|
+
previous,
|
|
5560
|
+
adjusted: hints.adjusted,
|
|
5561
|
+
current: snapshot
|
|
5562
|
+
})
|
|
4164
5563
|
});
|
|
4165
5564
|
const dropOrder = topologicalViewNames(
|
|
4166
5565
|
new Set([...affected].filter((name) => previousViews.has(name))),
|
|
@@ -4187,15 +5586,44 @@ async function planMigrationV2({
|
|
|
4187
5586
|
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
4188
5587
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
4189
5588
|
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
5589
|
+
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql7) => ({ kind: "ddl", sql: sql7 })),
|
|
4190
5590
|
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
4191
5591
|
...createOrder.map((name) => {
|
|
4192
5592
|
const view = currentViewDefinitions.get(name);
|
|
4193
5593
|
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
|
|
4194
5594
|
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
4195
|
-
})
|
|
5595
|
+
}),
|
|
5596
|
+
...refreshFunctionStatements({
|
|
5597
|
+
namespace: canonical.namespace,
|
|
5598
|
+
previous: previous._sequenceOrm?.refreshFunctions ?? {},
|
|
5599
|
+
current: materializedRefreshFunctions(canonical)
|
|
5600
|
+
}).map((sql7) => ({ kind: "ddl", sql: sql7 }))
|
|
4196
5601
|
];
|
|
4197
5602
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
4198
5603
|
}
|
|
5604
|
+
function orderForeignKeyAdditions(canonical, statements) {
|
|
5605
|
+
if (!canonical.tables.some((table) => table.foreignKeys?.length)) return [...statements];
|
|
5606
|
+
const addsForeignKey = (statement) => /^ALTER TABLE[\s\S]* ADD CONSTRAINT[\s\S]* FOREIGN KEY\b/.test(statement);
|
|
5607
|
+
return [...statements.filter((statement) => !addsForeignKey(statement)), ...statements.filter(addsForeignKey)];
|
|
5608
|
+
}
|
|
5609
|
+
function foreignKeyTimingStatements(canonical, previous, baseDdl) {
|
|
5610
|
+
const prior = previous._sequenceOrm?.foreignKeys ?? {};
|
|
5611
|
+
const result = [];
|
|
5612
|
+
for (const table of canonical.tables) {
|
|
5613
|
+
for (const fk of table.foreignKeys ?? []) {
|
|
5614
|
+
const before = prior[`${table.table}.${fk.name}`];
|
|
5615
|
+
const recreated = baseDdl.some((statement) => statement.includes(`ALTER TABLE ${quoteIdentifier(ormSchemaName(canonical.namespace))}.${quoteIdentifier(table.table)} ADD CONSTRAINT ${quoteIdentifier(fk.name)}`));
|
|
5616
|
+
if (fk.deferrable ? !before || before.deferrable !== fk.deferrable || recreated : before?.deferrable && !recreated) {
|
|
5617
|
+
result.push(foreignKeyTimingSql(ormSchemaName(canonical.namespace), table.table, fk.name, fk.deferrable));
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
return result;
|
|
5622
|
+
}
|
|
5623
|
+
function foreignKeyTimingSql(schema, table, name, timing) {
|
|
5624
|
+
const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
|
|
5625
|
+
return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name)} ${clause}`;
|
|
5626
|
+
}
|
|
4199
5627
|
function validateViewCatalogStatement({
|
|
4200
5628
|
canonical,
|
|
4201
5629
|
view,
|
|
@@ -4213,9 +5641,9 @@ function validateViewCatalogStatement({
|
|
|
4213
5641
|
}
|
|
4214
5642
|
const expectedDependencies = sqlTextArray(
|
|
4215
5643
|
view.dependencies.map((dependency) => {
|
|
4216
|
-
const
|
|
4217
|
-
if (!
|
|
4218
|
-
return
|
|
5644
|
+
const physical2 = relationIdentity.get(dependency);
|
|
5645
|
+
if (!physical2) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
5646
|
+
return physical2;
|
|
4219
5647
|
}).sort()
|
|
4220
5648
|
);
|
|
4221
5649
|
const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
@@ -4323,20 +5751,102 @@ function withoutViews(snapshot) {
|
|
|
4323
5751
|
delete clone._sequenceOrm;
|
|
4324
5752
|
return clone;
|
|
4325
5753
|
}
|
|
5754
|
+
var DROPS_TYPE = /^\s*DROP\s+TYPE\b/i;
|
|
5755
|
+
function viewReach({
|
|
5756
|
+
viewRecreation,
|
|
5757
|
+
hintStatements,
|
|
5758
|
+
baseDdl,
|
|
5759
|
+
previous,
|
|
5760
|
+
adjusted,
|
|
5761
|
+
current
|
|
5762
|
+
}) {
|
|
5763
|
+
if (hintStatements.length === 0 && baseDdl.length === 0) return /* @__PURE__ */ new Set();
|
|
5764
|
+
if (viewRecreation === "all-on-base-change") return "all";
|
|
5765
|
+
if (baseDdl.some((statement) => DROPS_TYPE.test(statement))) return "all";
|
|
5766
|
+
const dependencies = snapshotViewDependencies(adjusted);
|
|
5767
|
+
if ([...snapshotViews(adjusted).keys()].some((name) => dependencies[name] === void 0)) return "all";
|
|
5768
|
+
return baseChangesReachingViews({ previous, adjusted, current });
|
|
5769
|
+
}
|
|
5770
|
+
var SNAPSHOT_BOOKKEEPING = /* @__PURE__ */ new Set(["id", "prevId", "version", "dialect", "_meta", "views", "_sequenceOrm", "enums"]);
|
|
5771
|
+
var VIEW_SAFE_TABLE_FIELDS = /* @__PURE__ */ new Set([
|
|
5772
|
+
"indexes",
|
|
5773
|
+
"foreignKeys",
|
|
5774
|
+
"uniqueConstraints",
|
|
5775
|
+
"checkConstraints",
|
|
5776
|
+
"policies",
|
|
5777
|
+
"isRLSEnabled"
|
|
5778
|
+
]);
|
|
5779
|
+
var VIEW_SAFE_COLUMN_FIELDS = /* @__PURE__ */ new Set(["notNull", "default", "isUnique", "uniqueName", "nullsNotDistinct"]);
|
|
5780
|
+
function baseChangesReachingViews({
|
|
5781
|
+
previous,
|
|
5782
|
+
adjusted,
|
|
5783
|
+
current
|
|
5784
|
+
}) {
|
|
5785
|
+
const before = adjusted;
|
|
5786
|
+
const after = current;
|
|
5787
|
+
for (const section of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
5788
|
+
if (SNAPSHOT_BOOKKEEPING.has(section) || section === "tables") continue;
|
|
5789
|
+
if (stableJson(before[section]) !== stableJson(after[section])) return "all";
|
|
5790
|
+
}
|
|
5791
|
+
const relations = /* @__PURE__ */ new Set();
|
|
5792
|
+
const previousTables = snapshotTables(previous);
|
|
5793
|
+
const adjustedTables = snapshotTables(adjusted);
|
|
5794
|
+
for (const key of changedEntryKeys(previousTables, adjustedTables)) {
|
|
5795
|
+
const table = previousTables[key] ?? adjustedTables[key];
|
|
5796
|
+
if (table) relations.add(table.name);
|
|
5797
|
+
}
|
|
5798
|
+
const currentTables = snapshotTables(current);
|
|
5799
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(adjustedTables), ...Object.keys(currentTables)])) {
|
|
5800
|
+
const was = adjustedTables[key];
|
|
5801
|
+
const is2 = currentTables[key];
|
|
5802
|
+
if (!was || !is2) {
|
|
5803
|
+
const table = was ?? is2;
|
|
5804
|
+
if (table) relations.add(table.name);
|
|
5805
|
+
} else if (tableChangeReachesViews({ was, is: is2 })) {
|
|
5806
|
+
relations.add(is2.name);
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
return relations;
|
|
5810
|
+
}
|
|
5811
|
+
function tableChangeReachesViews({ was, is: is2 }) {
|
|
5812
|
+
for (const field of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(is2)])) {
|
|
5813
|
+
if (field === "columns" || VIEW_SAFE_TABLE_FIELDS.has(field)) continue;
|
|
5814
|
+
if (stableJson(was[field]) !== stableJson(is2[field])) return true;
|
|
5815
|
+
}
|
|
5816
|
+
return Object.entries(was.columns).some(([name, column]) => {
|
|
5817
|
+
const next = is2.columns[name];
|
|
5818
|
+
if (!next) return true;
|
|
5819
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(column), ...Object.keys(next)])].some(
|
|
5820
|
+
(field) => !VIEW_SAFE_COLUMN_FIELDS.has(field) && stableJson(column[field]) !== stableJson(next[field])
|
|
5821
|
+
);
|
|
5822
|
+
});
|
|
5823
|
+
}
|
|
5824
|
+
function changedEntryKeys(before, after) {
|
|
5825
|
+
return new Set(
|
|
5826
|
+
[.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter(
|
|
5827
|
+
(key) => stableJson(before[key]) !== stableJson(after[key])
|
|
5828
|
+
)
|
|
5829
|
+
);
|
|
5830
|
+
}
|
|
5831
|
+
function snapshotTables(snapshot) {
|
|
5832
|
+
return snapshot.tables ?? {};
|
|
5833
|
+
}
|
|
4326
5834
|
function affectedViewNames({
|
|
4327
5835
|
previousViews,
|
|
4328
5836
|
currentViews,
|
|
4329
5837
|
previousDependencies,
|
|
4330
5838
|
currentDependencies,
|
|
4331
|
-
|
|
5839
|
+
reach
|
|
4332
5840
|
}) {
|
|
4333
5841
|
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
4334
|
-
if (
|
|
5842
|
+
if (reach === "all") return allNames;
|
|
5843
|
+
const dependenciesOf = (name) => /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
|
|
4335
5844
|
const affected = new Set(
|
|
4336
5845
|
[...allNames].filter((name) => {
|
|
4337
5846
|
const before = previousViews.get(name);
|
|
4338
5847
|
const after = currentViews.get(name);
|
|
4339
|
-
|
|
5848
|
+
if (before === void 0 || after === void 0 || stableJson(before) !== stableJson(after)) return true;
|
|
5849
|
+
return [...dependenciesOf(name)].some((dependency) => reach.has(dependency));
|
|
4340
5850
|
})
|
|
4341
5851
|
);
|
|
4342
5852
|
let grew = true;
|
|
@@ -4344,8 +5854,7 @@ function affectedViewNames({
|
|
|
4344
5854
|
grew = false;
|
|
4345
5855
|
for (const name of allNames) {
|
|
4346
5856
|
if (affected.has(name)) continue;
|
|
4347
|
-
|
|
4348
|
-
if ([...dependencies].some((dependency) => affected.has(dependency))) {
|
|
5857
|
+
if ([...dependenciesOf(name)].some((dependency) => affected.has(dependency))) {
|
|
4349
5858
|
affected.add(name);
|
|
4350
5859
|
grew = true;
|
|
4351
5860
|
}
|
|
@@ -4450,7 +5959,63 @@ async function pendingKitStatementsV2({
|
|
|
4450
5959
|
canonical,
|
|
4451
5960
|
db
|
|
4452
5961
|
}) {
|
|
4453
|
-
|
|
5962
|
+
const { desired, defaultDrift } = await normalizeJsonDefaultsForIntrospection(canonical, db);
|
|
5963
|
+
const entities = buildDrizzleEntitiesV2(desired, { introspection: true });
|
|
5964
|
+
const pending = await pendingStatements({ entities, db, owned: V2_OWNED_STATEMENT });
|
|
5965
|
+
return [...orderForeignKeyAdditions(canonical, pending), ...defaultDrift, ...await pendingForeignKeyTimingStatements(canonical, db)];
|
|
5966
|
+
}
|
|
5967
|
+
async function normalizeJsonDefaultsForIntrospection(canonical, db) {
|
|
5968
|
+
const defaults = canonical.tables.flatMap((table, tableIndex) => table.source ? [] : table.columns.flatMap((column, columnIndex) => column.type === "jsonb" && typeof column.default === "string" ? [{ tableIndex, columnIndex, table: table.table, column: column.columnName, value: column.default }] : []));
|
|
5969
|
+
if (defaults.length === 0) return { desired: canonical, defaultDrift: [] };
|
|
5970
|
+
const array = (values) => sql`ARRAY[${sql.join(values.map((value) => sql`${value}`), sql`, `)}]::text[]`;
|
|
5971
|
+
const schema = ormSchemaName(canonical.namespace);
|
|
5972
|
+
const result = await shimExecute(db).execute(sql`
|
|
5973
|
+
SELECT desired.ordinal, desired.value::jsonb::text AS normalized,
|
|
5974
|
+
attribute.attnum IS NOT NULL AS column_exists,
|
|
5975
|
+
coalesce(pg_get_expr(ad.adbin, ad.adrelid) = quote_literal(desired.value::jsonb::text) || '::jsonb', false) AS matches
|
|
5976
|
+
FROM unnest(${array(defaults.map((item) => item.table))}, ${array(defaults.map((item) => item.column))}, ${array(defaults.map((item) => item.value))})
|
|
5977
|
+
WITH ORDINALITY AS desired(table_name, column_name, value, ordinal)
|
|
5978
|
+
LEFT JOIN pg_catalog.pg_namespace namespace ON namespace.nspname = ${schema}
|
|
5979
|
+
LEFT JOIN pg_catalog.pg_class relation ON relation.relnamespace = namespace.oid AND relation.relname = desired.table_name
|
|
5980
|
+
LEFT JOIN pg_catalog.pg_attribute attribute ON attribute.attrelid = relation.oid AND attribute.attname = desired.column_name AND NOT attribute.attisdropped
|
|
5981
|
+
LEFT JOIN pg_catalog.pg_attrdef ad ON ad.adrelid = relation.oid AND ad.adnum = attribute.attnum
|
|
5982
|
+
ORDER BY desired.ordinal
|
|
5983
|
+
`);
|
|
5984
|
+
const rows = result.rows;
|
|
5985
|
+
if (!Array.isArray(rows) || rows.length !== defaults.length || rows.some((row, index3) => Number(row.ordinal) !== index3 + 1 || typeof row.normalized !== "string" || typeof row.column_exists !== "boolean" || typeof row.matches !== "boolean")) {
|
|
5986
|
+
throw new Error("invalid JSON default introspection result");
|
|
5987
|
+
}
|
|
5988
|
+
const desired = structuredClone(canonical);
|
|
5989
|
+
const defaultDrift = [];
|
|
5990
|
+
defaults.forEach((item, index3) => {
|
|
5991
|
+
const row = rows[index3];
|
|
5992
|
+
desired.tables[item.tableIndex].columns[item.columnIndex].default = JSON.stringify(JSON.parse(row.normalized));
|
|
5993
|
+
if (row.column_exists && !row.matches) defaultDrift.push(
|
|
5994
|
+
`ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(item.table)} ALTER COLUMN ${quoteIdentifier(item.column)} SET DEFAULT ${quoteLiteral(item.value)}::jsonb;`
|
|
5995
|
+
);
|
|
5996
|
+
});
|
|
5997
|
+
return { desired, defaultDrift };
|
|
5998
|
+
}
|
|
5999
|
+
async function pendingForeignKeyTimingStatements(canonical, db) {
|
|
6000
|
+
const keys = canonical.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => ({ table: table.table, fk })));
|
|
6001
|
+
if (keys.length === 0) return [];
|
|
6002
|
+
const schema = ormSchemaName(canonical.namespace);
|
|
6003
|
+
const result = await shimExecute(db).execute(sql`
|
|
6004
|
+
SELECT relation.relname AS table_name, constraint_row.conname AS name,
|
|
6005
|
+
constraint_row.condeferrable AS deferrable, constraint_row.condeferred AS deferred
|
|
6006
|
+
FROM pg_catalog.pg_constraint AS constraint_row
|
|
6007
|
+
JOIN pg_catalog.pg_class AS relation ON relation.oid = constraint_row.conrelid
|
|
6008
|
+
JOIN pg_catalog.pg_namespace AS namespace ON namespace.oid = relation.relnamespace
|
|
6009
|
+
WHERE namespace.nspname = ${schema} AND constraint_row.contype = 'f'
|
|
6010
|
+
`);
|
|
6011
|
+
const rows = result.rows;
|
|
6012
|
+
return keys.flatMap(({ table, fk }) => {
|
|
6013
|
+
const actual = rows.find((row) => row.table_name === table && row.name === fk.name);
|
|
6014
|
+
const deferrable = fk.deferrable !== void 0;
|
|
6015
|
+
const deferred = fk.deferrable === "initially-deferred";
|
|
6016
|
+
if (actual ? actual.deferrable === deferrable && actual.deferred === deferred : !deferrable) return [];
|
|
6017
|
+
return [foreignKeyTimingSql(schema, table, fk.name, fk.deferrable)];
|
|
6018
|
+
});
|
|
4454
6019
|
}
|
|
4455
6020
|
async function pendingStatements({
|
|
4456
6021
|
entities,
|
|
@@ -4463,11 +6028,31 @@ async function pendingStatements({
|
|
|
4463
6028
|
);
|
|
4464
6029
|
return statementsToExecute.filter((statement) => !owned.test(statement));
|
|
4465
6030
|
}
|
|
6031
|
+
function correctIntrospectionQuery(query) {
|
|
6032
|
+
if (!is(query, SQL)) return query;
|
|
6033
|
+
const compiled = new PgDialect().sqlToQuery(query);
|
|
6034
|
+
let text4 = compiled.sql;
|
|
6035
|
+
const indexJoin = "pg_constraint con ON con.conindid = idx.indexrelid";
|
|
6036
|
+
if (text4.includes("AS generated_by_constraint") && text4.includes(indexJoin)) {
|
|
6037
|
+
text4 = text4.replace(indexJoin, `${indexJoin} AND con.contype IN ('p', 'u', 'x')`);
|
|
6038
|
+
}
|
|
6039
|
+
const sourceJoin = "LEFT JOIN pg_catalog.pg_attribute att ON att.attnum = ANY (con.conkey)";
|
|
6040
|
+
const targetJoin = "LEFT JOIN pg_catalog.pg_attribute fatt ON fatt.attnum = ANY (con.confkey)";
|
|
6041
|
+
if (text4.includes(sourceJoin) && text4.includes(targetJoin) && text4.includes("AND con.contype IN ('f')")) {
|
|
6042
|
+
text4 = text4.replace(
|
|
6043
|
+
sourceJoin,
|
|
6044
|
+
"JOIN LATERAL unnest(con.conkey, con.confkey) WITH ORDINALITY AS fk_key(source_num, target_num, ordinal) ON true LEFT JOIN pg_catalog.pg_attribute att ON att.attnum = fk_key.source_num"
|
|
6045
|
+
).replace(targetJoin, "LEFT JOIN pg_catalog.pg_attribute fatt ON fatt.attnum = fk_key.target_num").replace(/;\s*$/, "") + " ORDER BY con.conname, fk_key.ordinal";
|
|
6046
|
+
}
|
|
6047
|
+
if (text4 === compiled.sql) return query;
|
|
6048
|
+
if (compiled.params.length !== 0) throw new Error("parameterized Drizzle catalog query cannot be normalized");
|
|
6049
|
+
return sql.raw(text4);
|
|
6050
|
+
}
|
|
4466
6051
|
function shimExecute(db) {
|
|
4467
6052
|
const execute = db.execute.bind(db);
|
|
4468
6053
|
return {
|
|
4469
6054
|
execute: async (query) => {
|
|
4470
|
-
const result = await execute(query);
|
|
6055
|
+
const result = await execute(correctIntrospectionQuery(query));
|
|
4471
6056
|
if (result && typeof result === "object" && "rows" in result) return result;
|
|
4472
6057
|
return { rows: result };
|
|
4473
6058
|
}
|
|
@@ -4507,21 +6092,73 @@ async function planMigration({
|
|
|
4507
6092
|
const ddl = await diffStatements(hints.adjusted, snapshot);
|
|
4508
6093
|
const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
|
|
4509
6094
|
const statements = [
|
|
4510
|
-
...policyDrops.map((
|
|
4511
|
-
...hints.statements.map((
|
|
4512
|
-
...ddl.map((
|
|
4513
|
-
...securityStatements(definition).map((
|
|
6095
|
+
...policyDrops.map((sql7) => ({ kind: "security", sql: sql7 })),
|
|
6096
|
+
...hints.statements.map((sql7) => ({ kind: "hint", sql: sql7 })),
|
|
6097
|
+
...ddl.map((sql7) => ({ kind: "ddl", sql: sql7.replace(/;\s*$/, "") })),
|
|
6098
|
+
...securityStatements(definition).map((sql7) => ({ kind: "security", sql: sql7 })),
|
|
4514
6099
|
// PREPARE each action's executes first (plpgsql bodies don't check columns
|
|
4515
6100
|
// at CREATE), then create the functions. Both reference the tables the DDL
|
|
4516
6101
|
// above creates, so they close the plan.
|
|
4517
|
-
...actionPrepareChecks(definition).map((
|
|
4518
|
-
...actionFunctionStatements(definition).map((
|
|
6102
|
+
...actionPrepareChecks(definition).map((sql7) => ({ kind: "function", sql: sql7 })),
|
|
6103
|
+
...actionFunctionStatements(definition).map((sql7) => ({ kind: "function", sql: sql7 }))
|
|
4519
6104
|
];
|
|
4520
6105
|
return { namespace: definition.namespace, schema, statements, snapshot };
|
|
4521
6106
|
}
|
|
4522
6107
|
var compileNamespace = (definition) => planMigration({ definition });
|
|
4523
6108
|
var previousId2 = (snapshot) => snapshot.id;
|
|
4524
|
-
var
|
|
6109
|
+
var ORM_MIGRATION_EXPORT_FILE = "orm.migration.json";
|
|
6110
|
+
var ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
|
|
6111
|
+
var hash = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
6112
|
+
var ormMigrationPayloadSchema = z.object({
|
|
6113
|
+
canonical_definition: canonicalDefinitionSchema,
|
|
6114
|
+
documents: z.array(z.object({ path: z.string().min(1).max(2048), contents: z.string().max(1e6) }).strict()).max(5e3),
|
|
6115
|
+
client_manifest: z.object({ ops_hash: hash, operations: z.record(z.string(), hash) }).strict(),
|
|
6116
|
+
sdl: z.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_GENERATED_SDL_BYTES, {
|
|
6117
|
+
message: `sdl exceeds ${MAX_GENERATED_SDL_BYTES} bytes`
|
|
6118
|
+
}),
|
|
6119
|
+
schema_snapshot: z.json(),
|
|
6120
|
+
// The existing ORM committed-migration validator and replay engine remain
|
|
6121
|
+
// authoritative. This envelope hashes the provenance, never authorizes SQL.
|
|
6122
|
+
migration: z.json().optional()
|
|
6123
|
+
}).strict();
|
|
6124
|
+
var ormMigrationExportSchema = z.object({
|
|
6125
|
+
schema_version: z.literal(1),
|
|
6126
|
+
schema_hash: hash,
|
|
6127
|
+
payload: ormMigrationPayloadSchema,
|
|
6128
|
+
checksum: z.string().regex(/^[0-9a-f]{64}$/)
|
|
6129
|
+
}).strict();
|
|
6130
|
+
function checksum(value) {
|
|
6131
|
+
return createHash("sha256").update(stableJson(value)).digest("hex");
|
|
6132
|
+
}
|
|
6133
|
+
function createOrmMigrationExport({ schemaHash, payload }) {
|
|
6134
|
+
const contents = { schema_version: 1, schema_hash: hash.parse(schemaHash), payload: ormMigrationPayloadSchema.parse(payload) };
|
|
6135
|
+
return { ...contents, checksum: checksum(contents) };
|
|
6136
|
+
}
|
|
6137
|
+
function parseOrmMigrationExport(text4) {
|
|
6138
|
+
if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
|
|
6139
|
+
const exported = ormMigrationExportSchema.parse(JSON.parse(text4));
|
|
6140
|
+
const { checksum: expected, ...contents } = exported;
|
|
6141
|
+
if (checksum(contents) !== expected) throw new Error("ORM migration export checksum mismatch; regenerate and commit it");
|
|
6142
|
+
return exported;
|
|
6143
|
+
}
|
|
6144
|
+
function renderOrmMigrationExport(exported) {
|
|
6145
|
+
const text4 = `${JSON.stringify(exported, null, 2)}
|
|
6146
|
+
`;
|
|
6147
|
+
if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
|
|
6148
|
+
return text4;
|
|
6149
|
+
}
|
|
6150
|
+
var IR_VERSION = 2;
|
|
6151
|
+
var LEGACY_IR_VERSION = 1;
|
|
6152
|
+
var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(IR_VERSION)]);
|
|
6153
|
+
var operationVersionSchema = z.object({ irVersion: irVersionSchema });
|
|
6154
|
+
function requiredArtifactIrVersion(operations) {
|
|
6155
|
+
let required = LEGACY_IR_VERSION;
|
|
6156
|
+
for (const operation of operations) {
|
|
6157
|
+
const { irVersion } = operationVersionSchema.parse(operation);
|
|
6158
|
+
if (irVersion > required) required = irVersion;
|
|
6159
|
+
}
|
|
6160
|
+
return required;
|
|
6161
|
+
}
|
|
4525
6162
|
var CLAIMS_CONTRACT = {
|
|
4526
6163
|
sub: "string",
|
|
4527
6164
|
org: "string?",
|
|
@@ -4593,6 +6230,21 @@ var selectionIrSchema = z.lazy(
|
|
|
4593
6230
|
() => z.object({
|
|
4594
6231
|
typename: z.string(),
|
|
4595
6232
|
columns: z.array(z.object({ alias: z.string(), column: z.string() })),
|
|
6233
|
+
cells: z.object({
|
|
6234
|
+
alias: z.string(),
|
|
6235
|
+
typename: z.string(),
|
|
6236
|
+
fields: z.array(z.object({
|
|
6237
|
+
alias: z.string(),
|
|
6238
|
+
column: z.string(),
|
|
6239
|
+
typename: z.string(),
|
|
6240
|
+
parts: z.array(z.object({
|
|
6241
|
+
alias: z.string(),
|
|
6242
|
+
part: z.enum(["source", "at", "by", "conflicts"]),
|
|
6243
|
+
fields: z.array(z.object({ alias: z.string(), field: z.enum(["source", "at", "value"]) })).optional(),
|
|
6244
|
+
typename: z.string().optional()
|
|
6245
|
+
}))
|
|
6246
|
+
}))
|
|
6247
|
+
}).optional(),
|
|
4596
6248
|
relations: z.array(z.object({
|
|
4597
6249
|
alias: z.string(),
|
|
4598
6250
|
link: z.string(),
|
|
@@ -4614,6 +6266,11 @@ var selectionIrSchema = z.lazy(
|
|
|
4614
6266
|
})
|
|
4615
6267
|
);
|
|
4616
6268
|
var includeIfSchema = z.object({ variable: z.string(), negated: z.boolean() });
|
|
6269
|
+
var prefetchPlanIrSchema = z.object({
|
|
6270
|
+
source: z.string(),
|
|
6271
|
+
maxStalenessMs: z.number().int().positive().optional(),
|
|
6272
|
+
key: z.union([z.object({ one: valueIrSchema }), z.object({ many: valueIrSchema })])
|
|
6273
|
+
});
|
|
4617
6274
|
var selectRootSchema = z.object({
|
|
4618
6275
|
kind: z.literal("select"),
|
|
4619
6276
|
alias: z.string(),
|
|
@@ -4625,7 +6282,8 @@ var selectRootSchema = z.object({
|
|
|
4625
6282
|
distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
|
|
4626
6283
|
limit: limitIrSchema,
|
|
4627
6284
|
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
4628
|
-
includeIf: includeIfSchema.optional()
|
|
6285
|
+
includeIf: includeIfSchema.optional(),
|
|
6286
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
4629
6287
|
});
|
|
4630
6288
|
var byPkRootSchema = z.object({
|
|
4631
6289
|
kind: z.literal("byPk"),
|
|
@@ -4635,7 +6293,8 @@ var byPkRootSchema = z.object({
|
|
|
4635
6293
|
pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
|
|
4636
6294
|
/** Policy predicate — ANDed with the PK match. */
|
|
4637
6295
|
where: predIrSchema,
|
|
4638
|
-
includeIf: includeIfSchema.optional()
|
|
6296
|
+
includeIf: includeIfSchema.optional(),
|
|
6297
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
4639
6298
|
});
|
|
4640
6299
|
var aggregateRootSchema = z.object({
|
|
4641
6300
|
kind: z.literal("aggregate"),
|
|
@@ -4648,6 +6307,33 @@ var aggregateRootSchema = z.object({
|
|
|
4648
6307
|
nodes: selectionIrSchema.optional(),
|
|
4649
6308
|
includeIf: includeIfSchema.optional()
|
|
4650
6309
|
});
|
|
6310
|
+
var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
|
|
6311
|
+
var groupByKeyIrSchema = z.object({
|
|
6312
|
+
column: z.string(),
|
|
6313
|
+
bucket: z.enum(DATE_BUCKETS).optional()
|
|
6314
|
+
});
|
|
6315
|
+
var aggregateGroupsRootSchema = z.object({
|
|
6316
|
+
kind: z.literal("aggregateGroups"),
|
|
6317
|
+
alias: z.string(),
|
|
6318
|
+
table: z.string(),
|
|
6319
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
6320
|
+
where: predIrSchema,
|
|
6321
|
+
groupBy: z.array(groupByKeyIrSchema).min(1),
|
|
6322
|
+
/**
|
|
6323
|
+
* The `key { … }` projection: which group columns appear in each entry's key
|
|
6324
|
+
* object, in selection order. A subset of `groupBy` (the compiler rejects a
|
|
6325
|
+
* key field that is not a group key — it would be indeterminate within the
|
|
6326
|
+
* group). Empty when the document selects only `aggregate`.
|
|
6327
|
+
*/
|
|
6328
|
+
key: z.array(z.object({ alias: z.string(), column: z.string() })).default([]),
|
|
6329
|
+
count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
|
|
6330
|
+
sum: z.array(z.string()).optional(),
|
|
6331
|
+
avg: z.array(z.string()).optional(),
|
|
6332
|
+
/** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
|
|
6333
|
+
limit: limitIrSchema,
|
|
6334
|
+
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
6335
|
+
includeIf: includeIfSchema.optional()
|
|
6336
|
+
});
|
|
4651
6337
|
var writeValuesSchema = z.union([
|
|
4652
6338
|
z.object({ slot: z.string() }),
|
|
4653
6339
|
z.object({ entries: z.array(z.object({ column: z.string(), value: valueIrSchema })) })
|
|
@@ -4666,7 +6352,20 @@ var mutationRootSchema = z.object({
|
|
|
4666
6352
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
4667
6353
|
* row and carry the write policy in `where` like update/delete.
|
|
4668
6354
|
*/
|
|
4669
|
-
op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
6355
|
+
op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
|
|
6356
|
+
/**
|
|
6357
|
+
* resolvedWrite: a write to a resolved table (`table` is its view). The
|
|
6358
|
+
* executor gates on the resolved row (write policy in `where`), writes only
|
|
6359
|
+
* the touched cells into the manual source, then re-checks `insertCheck`
|
|
6360
|
+
* against the resolved row. `revert` clears `revertColumns` from the manual
|
|
6361
|
+
* source; `delete` tombstones the row there.
|
|
6362
|
+
*/
|
|
6363
|
+
resolvedAction: z.enum(["insert", "update", "delete", "revert"]).optional(),
|
|
6364
|
+
revertColumns: z.array(z.string()).optional(),
|
|
6365
|
+
/** `@writeback(source:, mode:)` directives on this root. */
|
|
6366
|
+
writeback: z.array(z.object({ source: z.string(), mode: z.enum(["sync", "async"]) })).optional(),
|
|
6367
|
+
/** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
|
|
6368
|
+
localOnly: z.array(z.string()).optional(),
|
|
4670
6369
|
alias: z.string(),
|
|
4671
6370
|
table: z.string(),
|
|
4672
6371
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -4693,12 +6392,15 @@ var mutationRootSchema = z.object({
|
|
|
4693
6392
|
insertCheck: predIrSchema.optional(),
|
|
4694
6393
|
returning: selectionIrSchema.nullable(),
|
|
4695
6394
|
affectedRows: z.boolean().default(false),
|
|
6395
|
+
/** v2: abort the transaction when the actual write count differs, regardless of selection. */
|
|
6396
|
+
assertAffectedRows: z.number().int().min(0).max(2147483647).optional(),
|
|
4696
6397
|
includeIf: includeIfSchema.optional()
|
|
4697
6398
|
});
|
|
4698
6399
|
var rootIrSchema = z.discriminatedUnion("kind", [
|
|
4699
6400
|
selectRootSchema,
|
|
4700
6401
|
byPkRootSchema,
|
|
4701
6402
|
aggregateRootSchema,
|
|
6403
|
+
aggregateGroupsRootSchema,
|
|
4702
6404
|
mutationRootSchema
|
|
4703
6405
|
]);
|
|
4704
6406
|
var INPUT_REF_KINDS = [
|
|
@@ -4728,7 +6430,7 @@ var variableIrSchema = z.object({
|
|
|
4728
6430
|
defaultValue: variableDefaultIrSchema.optional()
|
|
4729
6431
|
});
|
|
4730
6432
|
var compiledOperationSchema = z.object({
|
|
4731
|
-
irVersion:
|
|
6433
|
+
irVersion: irVersionSchema,
|
|
4732
6434
|
name: z.string(),
|
|
4733
6435
|
opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
4734
6436
|
kind: z.enum(["query", "mutation"]),
|
|
@@ -4740,6 +6442,50 @@ var compiledOperationSchema = z.object({
|
|
|
4740
6442
|
/** Every table the op touches — data-token scoping + audit. */
|
|
4741
6443
|
tableSet: z.array(z.string()).min(1),
|
|
4742
6444
|
roots: z.array(rootIrSchema).min(1)
|
|
6445
|
+
}).superRefine((operation, ctx) => {
|
|
6446
|
+
for (const [index3, root] of operation.roots.entries()) {
|
|
6447
|
+
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
6448
|
+
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
6449
|
+
ctx.addIssue({ code: "custom", path: ["roots", index3], message: "affected-row assertions require unconditional mutation roots and IR v2" });
|
|
6450
|
+
}
|
|
6451
|
+
}
|
|
6452
|
+
});
|
|
6453
|
+
var resolvedSourceIrSchema = z.object({
|
|
6454
|
+
name: z.string(),
|
|
6455
|
+
kind: z.enum(["manual", "etl", "prefetch"]),
|
|
6456
|
+
/** Logical table key of the backing relation. */
|
|
6457
|
+
table: z.string(),
|
|
6458
|
+
columns: z.array(z.string()),
|
|
6459
|
+
key: z.string().optional(),
|
|
6460
|
+
fetch: z.union([
|
|
6461
|
+
z.object({ shape: z.literal("single") }),
|
|
6462
|
+
z.object({ shape: z.literal("batch"), maxKeys: z.number().int().positive() })
|
|
6463
|
+
]).optional(),
|
|
6464
|
+
function: z.object({ kind: z.enum(["managed", "native"]), id: z.string() }).optional(),
|
|
6465
|
+
prefetchDefaults: z.object({
|
|
6466
|
+
maxStalenessMs: z.number().int().positive(),
|
|
6467
|
+
timeoutMs: z.number().int().positive(),
|
|
6468
|
+
onError: z.enum(["fallback", "fail"])
|
|
6469
|
+
}).optional(),
|
|
6470
|
+
writeback: z.object({
|
|
6471
|
+
columns: z.array(z.string()),
|
|
6472
|
+
defaultMode: z.enum(["sync", "async"]),
|
|
6473
|
+
timeoutMs: z.number().int().positive(),
|
|
6474
|
+
clearManualOnConfirm: z.boolean()
|
|
6475
|
+
}).optional()
|
|
6476
|
+
});
|
|
6477
|
+
var resolvedMetaIrSchema = z.object({
|
|
6478
|
+
/** Logical pk column of the resolved view (single-column). */
|
|
6479
|
+
pk: z.string(),
|
|
6480
|
+
/** Source name of the manual source. */
|
|
6481
|
+
manual: z.string(),
|
|
6482
|
+
sources: z.array(resolvedSourceIrSchema),
|
|
6483
|
+
deletedBy: z.array(z.string()),
|
|
6484
|
+
/**
|
|
6485
|
+
* Materialized: the view reads `table`, which the data plane refreshes from
|
|
6486
|
+
* the `compute` view (claims-gated) whenever it writes a source row.
|
|
6487
|
+
*/
|
|
6488
|
+
materialized: z.object({ table: z.string(), compute: z.string() }).optional()
|
|
4743
6489
|
});
|
|
4744
6490
|
var tableMetaIrSchema = z.object({
|
|
4745
6491
|
table: z.string(),
|
|
@@ -4776,18 +6522,45 @@ var tableMetaIrSchema = z.object({
|
|
|
4776
6522
|
insertable: z.array(z.string()),
|
|
4777
6523
|
settable: z.array(z.string()),
|
|
4778
6524
|
appendable: z.array(z.string()),
|
|
6525
|
+
/**
|
|
6526
|
+
* Logical columns stamped with Postgres `now()` on UPDATE (canonical
|
|
6527
|
+
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
6528
|
+
*/
|
|
6529
|
+
touchOnUpdate: z.array(z.string()).optional(),
|
|
6530
|
+
/**
|
|
6531
|
+
* Per-cell metadata: logical column → the logical names of its cell-part
|
|
6532
|
+
* columns (which are listed in `columns`, but never filterable, orderable or
|
|
6533
|
+
* writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
|
|
6534
|
+
* write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
|
|
6535
|
+
* the relation tracks no cells.
|
|
6536
|
+
*/
|
|
6537
|
+
cells: z.record(z.string(), z.object({
|
|
6538
|
+
at: z.string().optional(),
|
|
6539
|
+
by: z.string().optional(),
|
|
6540
|
+
rev: z.string().optional(),
|
|
6541
|
+
source: z.string().optional(),
|
|
6542
|
+
conflicts: z.string().optional()
|
|
6543
|
+
}).strict()).optional(),
|
|
6544
|
+
/** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
|
|
6545
|
+
resolved: resolvedMetaIrSchema.optional(),
|
|
4779
6546
|
constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
|
|
4780
6547
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
4781
6548
|
dynamicFilterMaxDepth: z.number().int().positive().default(6),
|
|
4782
6549
|
dynamicFilterMaxNodes: z.number().int().positive().default(64)
|
|
4783
6550
|
});
|
|
4784
6551
|
var compiledArtifactSchema = z.object({
|
|
4785
|
-
irVersion:
|
|
6552
|
+
irVersion: irVersionSchema,
|
|
4786
6553
|
namespace: z.string(),
|
|
4787
6554
|
schemaHash: z.string(),
|
|
4788
6555
|
opsHash: z.string(),
|
|
4789
6556
|
tables: z.record(z.string(), tableMetaIrSchema),
|
|
4790
6557
|
operations: z.record(z.string(), compiledOperationSchema)
|
|
6558
|
+
}).superRefine((artifact, ctx) => {
|
|
6559
|
+
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
6560
|
+
if (operation.irVersion > artifact.irVersion) {
|
|
6561
|
+
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
4791
6564
|
});
|
|
4792
6565
|
var IrVersionError = class extends Error {
|
|
4793
6566
|
constructor(found) {
|
|
@@ -4798,7 +6571,7 @@ var IrVersionError = class extends Error {
|
|
|
4798
6571
|
function normalizeIr(raw) {
|
|
4799
6572
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
4800
6573
|
const version = raw.irVersion;
|
|
4801
|
-
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
6574
|
+
if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
|
|
4802
6575
|
return compiledArtifactSchema.parse(raw);
|
|
4803
6576
|
}
|
|
4804
6577
|
function indexByHash(artifact) {
|
|
@@ -5114,12 +6887,12 @@ async function writeMigration({
|
|
|
5114
6887
|
const tip = workspace.journal.entries.at(-1);
|
|
5115
6888
|
const ordinal = (tip?.ordinal ?? 0) + 1;
|
|
5116
6889
|
const name = migrationName(ordinal, contentHash);
|
|
5117
|
-
const
|
|
6890
|
+
const sql7 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
|
|
5118
6891
|
const metaDir = join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
|
|
5119
6892
|
await mkdir(metaDir, { recursive: true });
|
|
5120
6893
|
const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
|
|
5121
6894
|
`;
|
|
5122
|
-
await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`),
|
|
6895
|
+
await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql7);
|
|
5123
6896
|
await writeFile(join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
|
|
5124
6897
|
const journal = {
|
|
5125
6898
|
...workspace.journal,
|
|
@@ -5130,7 +6903,7 @@ async function writeMigration({
|
|
|
5130
6903
|
name,
|
|
5131
6904
|
content_hash: contentHash,
|
|
5132
6905
|
previous_content_hash: tip?.content_hash ?? null,
|
|
5133
|
-
checksum: checksumOf(
|
|
6906
|
+
checksum: checksumOf(sql7),
|
|
5134
6907
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5135
6908
|
destructive
|
|
5136
6909
|
}
|
|
@@ -5245,8 +7018,8 @@ async function checkWorkspace({
|
|
|
5245
7018
|
let previousId3;
|
|
5246
7019
|
let previousHash = null;
|
|
5247
7020
|
for (const entry of workspace.journal.entries) {
|
|
5248
|
-
const
|
|
5249
|
-
if (checksumOf(
|
|
7021
|
+
const sql7 = await readMigrationSql(dir, entry);
|
|
7022
|
+
if (checksumOf(sql7) !== entry.checksum) {
|
|
5250
7023
|
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
5251
7024
|
return 1;
|
|
5252
7025
|
}
|
|
@@ -5286,7 +7059,7 @@ async function checkWorkspace({
|
|
|
5286
7059
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
5287
7060
|
return 0;
|
|
5288
7061
|
}
|
|
5289
|
-
var firstLine = (
|
|
7062
|
+
var firstLine = (sql7) => sql7.split("\n")[0] ?? sql7;
|
|
5290
7063
|
|
|
5291
7064
|
// src/migrations/from-yaml.ts
|
|
5292
7065
|
function convertV1DefinitionToCanonical(definition) {
|
|
@@ -5392,7 +7165,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5392
7165
|
throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
|
|
5393
7166
|
}
|
|
5394
7167
|
const name = migrationName(ordinal, newHash);
|
|
5395
|
-
const
|
|
7168
|
+
const sql7 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
|
|
5396
7169
|
const snapshotText = `${JSON.stringify(snapshot, null, 2)}
|
|
5397
7170
|
`;
|
|
5398
7171
|
const entry = {
|
|
@@ -5400,11 +7173,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5400
7173
|
name,
|
|
5401
7174
|
content_hash: newHash,
|
|
5402
7175
|
previous_content_hash: previousHash,
|
|
5403
|
-
checksum: checksumOf(
|
|
7176
|
+
checksum: checksumOf(sql7),
|
|
5404
7177
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5405
7178
|
destructive: false
|
|
5406
7179
|
};
|
|
5407
|
-
return { name, entry, sql:
|
|
7180
|
+
return { name, entry, sql: sql7, snapshotText };
|
|
5408
7181
|
}
|
|
5409
7182
|
function renderRebaseMigrationSql({
|
|
5410
7183
|
name,
|
|
@@ -5502,6 +7275,6 @@ function canonicalActions(value) {
|
|
|
5502
7275
|
);
|
|
5503
7276
|
}
|
|
5504
7277
|
|
|
5505
|
-
export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CANONICAL_FORMAT, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CLAIMS_CONTRACT, CLAIMS_SETTING, CLAIM_KEY_PATTERN, CMP_OPS, COLUMN_BRAND, COLUMN_DEFAULT_FNS, COLUMN_TYPES2 as COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, ColumnBuilder, DEFINITION_FILE_EXTENSIONS, ENUM_BRAND, HASH_DOMAIN, IDENTIFIER_PATTERN, IDENTIFIER_RE, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LINK_BRAND, MAX_DEFINITION_FILE_BYTES, MAX_IDENTIFIER_LENGTH, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, ORM_TO_PIPELINE_TYPE, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, ROLES_CLAIM_KEY, SCHEMA_DIR, STATEMENT_KINDS, SYNCED_CONTRACT_HASH_DOMAIN, SYNCED_SOURCE_PATTERN, SYNCED_VIEW_EXECUTION_OWNER_ROLE, TABLE_BRAND, VIEW_BRAND, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, activeAuthoringContext, applyMigrationHints, applyMigrationHintsV2, beginAuthoringContext, buildActionParamsSchema, buildDrizzleEntities, buildDrizzleEntitiesV2, buildNamespaceDefinition, buildRebaseJournalEntry, canonicalActionSchema, canonicalColumnSchema, canonicalDefinitionSchema, canonicalEnumSchema, canonicalIndexSchema, canonicalLinkSchema, canonicalPoliciesSchema, canonicalRelations, canonicalTableSchema, canonicalViewSchema, canonicalizeDefinition, capabilityExpression, castParts, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema2 as columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredIrToSql, compilePredicate, compiledArtifactSchema, compiledOperationSchema, computeCanonicalHash, computeDefinitionHash, computeSyncedContractHash, consumerRoleName, convertV1DefinitionToCanonical, declaredCapabilityKeys, defineEnum, defineNamespace, defineSyncedTable, defineTable, defineView, deriveColumnName, diffCommand, diffStatements, emptyJournal, emptySnapshot, endAuthoringContext, extractNamedParams, extractOrmSyncedTableContract, generateNamespaceTypes, generateSnapshot, generateSnapshotV2, identifierSchema, indexByHash, isColumnBuilder, isEnumRef, isFunctionDefault, isOwnedStatement, isOwnedStatementV2, isQualifiedReference, isSeqLink, isSeqNamespaceConfig, isSeqTable, isSeqView, limitIrSchema, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, loadNamespaceFromDir, migrationName, namespaceConfigSchema, namespaceSchema, normalizeCanonical, normalizeIr, normalizeSnapshot, orderIrSchema, ormSchemaName, overlayWinsUnion, overrideRowMutation, parseCanonicalDefinition, parseDefinitionSources, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, pendingKitStatementsV2, planMigration, planMigrationV2, policyExpressionSchema, policyVariantSchema, predIrSchema, predicateOperands, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, rebaseViolations, referencedCapabilities, referencedColumns, renderMigrationSql, revertOverlayMutation, rewriteSyncedSourceRelationsInViewQuery, roleExpression, rootIrSchema, securityStatements, selectionIrSchema, serializeNamespace, sqlTypeName, stableJson, syncedBackingRevokeStatements, syncedContractSurface, syncedTables, syncedViewDropStatements, syncedViewStatements, t, tableFileSchema, tableMetaIrSchema, tableReferenceSchema, toMany, toOne, toPlatformUser, typesInterfaceName, validateViewCatalogStatement, valueIrSchema, variableDefaultIrSchema, variableIrSchema, verifyRebaseIsNoop, walkPredicate, writeMigration };
|
|
7278
|
+
export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CANONICAL_FORMAT, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CELLS_FIELD, CELL_PARTS, CELL_PART_SUFFIX, CLAIMS_CONTRACT, CLAIMS_SETTING, CLAIM_KEY_PATTERN, CMP_OPS, COLUMN_BRAND, COLUMN_DEFAULT_FNS, COLUMN_IDENTIFIER_PATTERN, COLUMN_TYPES2 as COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, ColumnBuilder, DATE_BUCKETS, DEFINITION_FILE_EXTENSIONS, ENUM_BRAND, HASH_DOMAIN, IDENTIFIER_PATTERN, IDENTIFIER_RE, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LEGACY_IR_VERSION, LINK_BRAND, MATERIALIZE_ACTOR, MATERIALIZE_CLAIMS_JSON, MAX_DEFINITION_FILE_BYTES, MAX_GENERATED_SDL_BYTES, MAX_IDENTIFIER_LENGTH, MAX_TRACKED_COLUMN_NAME, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_MIGRATION_EXPORT_FILE, ORM_MIGRATION_EXPORT_MAX_BYTES, ORM_REGISTRATION_MAX_BYTES, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, ORM_TO_PIPELINE_TYPE, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, ROLES_CLAIM_KEY, SCHEMA_DIR, STATEMENT_KINDS, SYNCED_CONTRACT_HASH_DOMAIN, SYNCED_SOURCE_PATTERN, SYNCED_VIEW_EXECUTION_OWNER_ROLE, TABLE_BRAND, VIEW_BRAND, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, activeAuthoringContext, applyMigrationHints, applyMigrationHintsV2, beginAuthoringContext, buildActionParamsSchema, buildDrizzleEntities, buildDrizzleEntitiesV2, buildNamespaceDefinition, buildRebaseJournalEntry, canonicalActionSchema, canonicalColumnSchema, canonicalDefinitionSchema, canonicalEnumSchema, canonicalIndexSchema, canonicalLinkSchema, canonicalPoliciesSchema, canonicalRelations, canonicalTableSchema, canonicalViewSchema, canonicalizeDefinition, capabilityExpression, castParts, cellColumnName, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema2 as columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredIrToSql, compilePredicate, compiledArtifactSchema, compiledOperationSchema, computeCanonicalHash, computeDefinitionHash, computeSyncedContractHash, consumerRoleName, convertV1DefinitionToCanonical, createOrmMigrationExport, custom, declaredCapabilityKeys, defineEnum, defineNamespace, defineResolvedTable, defineSyncedTable, defineTable, defineView, deriveColumnName, diffCommand, diffStatements, emptyJournal, emptySnapshot, endAuthoringContext, etlSource, extractNamedParams, extractOrmSyncedTableContract, generateNamespaceTypes, generateSnapshot, generateSnapshotV2, highest, identifierSchema, indexByHash, isColumnBuilder, isEnumRef, isFunctionDefault, isOwnedStatement, isOwnedStatementV2, isQualifiedReference, isSeqLink, isSeqNamespaceConfig, isSeqTable, isSeqView, latest, limitIrSchema, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, loadNamespaceFromDir, lowest, managedFn, manualSource, materializeLockKey, materializedRefreshFunctionName, migrationName, namespaceConfigSchema, namespaceSchema, nonNullFirst, normalizeCanonical, normalizeIr, normalizeSnapshot, orderIrSchema, ormMigrationExportSchema, ormSchemaName, overlayWinsUnion, overrideRowMutation, parseCanonicalDefinition, parseDefinitionSources, parseOrmMigrationExport, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, pendingKitStatementsV2, planCommittedMigrationV2, planMigration, planMigrationV2, policyExpressionSchema, policyVariantSchema, predIrSchema, predicateOperands, prefetchPlanIrSchema, prefetchSource, priority, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, rebaseViolations, referencedCapabilities, referencedColumns, renderMigrationSql, renderOrmMigrationExport, requiredArtifactIrVersion, resolvedMetaIrSchema, resolvedSourceIrSchema, revertOverlayMutation, rewriteSyncedSourceRelationsInViewQuery, roleExpression, rootIrSchema, securityStatements, selectionIrSchema, serializeNamespace, sqlTypeName, stableJson, syncedBackingRevokeStatements, syncedContractSurface, syncedTables, syncedViewDropStatements, syncedViewStatements, t, tableFileSchema, tableMetaIrSchema, tableReferenceSchema, toMany, toOne, toPlatformUser, typesInterfaceName, validateViewCatalogStatement, valueIrSchema, variableDefaultIrSchema, variableIrSchema, verifyRebaseIsNoop, walkPredicate, writeMigration };
|
|
5506
7279
|
//# sourceMappingURL=index.js.map
|
|
5507
7280
|
//# sourceMappingURL=index.js.map
|