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