@sequenceholdings/orm 2.1.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 +38 -1
- package/dist/authoring.cjs +1358 -45
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +223 -2
- package/dist/authoring.d.ts +223 -2
- package/dist/authoring.js +1349 -47
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +662 -38
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +696 -53
- package/dist/canonical.d.ts +696 -53
- package/dist/canonical.js +647 -39
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +1808 -247
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1809 -248
- 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 +1325 -285
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +23 -2
- package/dist/compiler.d.ts +23 -2
- package/dist/compiler.js +1325 -285
- 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 +1605 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +230 -113
- package/dist/index.d.ts +230 -113
- package/dist/index.js +1583 -142
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +727 -38
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +539 -21
- package/dist/ir.d.ts +539 -21
- package/dist/ir.js +722 -39
- package/dist/ir.js.map +1 -1
- package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
- package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +726 -42
- 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 +726 -42
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +858 -54
- 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 +856 -55
- package/dist/sdl.js.map +1 -1
- package/package.json +9 -4
- 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');
|
|
@@ -40,7 +40,8 @@ var ColumnBuilder = class {
|
|
|
40
40
|
description: void 0,
|
|
41
41
|
renamedFrom: void 0,
|
|
42
42
|
cast: void 0,
|
|
43
|
-
deprecated: false
|
|
43
|
+
deprecated: false,
|
|
44
|
+
cell: void 0
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
/** Marks this column as (part of) the primary key. Implies NOT NULL. */
|
|
@@ -100,6 +101,11 @@ var ColumnBuilder = class {
|
|
|
100
101
|
this.state.deprecated = true;
|
|
101
102
|
return this;
|
|
102
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
|
+
}
|
|
103
109
|
/** Whether `.primaryKey()` was called — collected into the table-level PK. */
|
|
104
110
|
get isPrimaryKey() {
|
|
105
111
|
return this.state.primaryKey;
|
|
@@ -128,6 +134,7 @@ var ColumnBuilder = class {
|
|
|
128
134
|
if (this.state.description !== void 0) column.description = this.state.description;
|
|
129
135
|
if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
|
|
130
136
|
if (this.state.cast !== void 0) column.cast = this.state.cast;
|
|
137
|
+
if (this.state.cell !== void 0) column.cell = { ...this.state.cell };
|
|
131
138
|
return column;
|
|
132
139
|
}
|
|
133
140
|
};
|
|
@@ -151,12 +158,12 @@ var t = {
|
|
|
151
158
|
|
|
152
159
|
// src/constants.ts
|
|
153
160
|
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
161
|
+
var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
|
|
162
|
+
var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
|
|
154
163
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
155
164
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
156
165
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
157
|
-
var
|
|
158
|
-
var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
|
|
159
|
-
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
166
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
160
167
|
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
161
168
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
162
169
|
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
@@ -172,6 +179,27 @@ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
|
|
|
172
179
|
var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
|
|
173
180
|
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
174
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
|
+
}
|
|
175
203
|
|
|
176
204
|
// src/errors.ts
|
|
177
205
|
var OrmDefinitionError = class extends Error {
|
|
@@ -247,6 +275,8 @@ function toOne(target, options) {
|
|
|
247
275
|
on: options.on,
|
|
248
276
|
references: options.references,
|
|
249
277
|
enforce: options.enforce ?? false,
|
|
278
|
+
onDelete: options.onDelete,
|
|
279
|
+
onUpdate: options.onUpdate,
|
|
250
280
|
description: options.description
|
|
251
281
|
};
|
|
252
282
|
}
|
|
@@ -258,6 +288,8 @@ function toMany(target, options) {
|
|
|
258
288
|
on: options.on,
|
|
259
289
|
references: options.references,
|
|
260
290
|
enforce: false,
|
|
291
|
+
onDelete: void 0,
|
|
292
|
+
onUpdate: void 0,
|
|
261
293
|
description: options.description
|
|
262
294
|
};
|
|
263
295
|
}
|
|
@@ -269,6 +301,8 @@ function toPlatformUser(options) {
|
|
|
269
301
|
on: options.on,
|
|
270
302
|
references: options.references,
|
|
271
303
|
enforce: false,
|
|
304
|
+
onDelete: void 0,
|
|
305
|
+
onUpdate: void 0,
|
|
272
306
|
description: options.description
|
|
273
307
|
};
|
|
274
308
|
}
|
|
@@ -303,6 +337,12 @@ function defineTable(name, columns, config = {}) {
|
|
|
303
337
|
}
|
|
304
338
|
const columnName = builder.columnNameFor(propertyName);
|
|
305
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
|
+
}
|
|
306
346
|
const existing = seenColumnNames.get(columnName);
|
|
307
347
|
if (existing !== void 0) {
|
|
308
348
|
throw new OrmDefinitionError(
|
|
@@ -550,11 +590,11 @@ function compileViewQueries(input) {
|
|
|
550
590
|
);
|
|
551
591
|
}
|
|
552
592
|
if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
|
|
553
|
-
const tableDependencies = pgCore.extractUsedTable(config.query).map((
|
|
554
|
-
const logical = physicalToLogical.get(
|
|
593
|
+
const tableDependencies = pgCore.extractUsedTable(config.query).map((physical2) => {
|
|
594
|
+
const logical = physicalToLogical.get(physical2);
|
|
555
595
|
if (!logical) {
|
|
556
596
|
throw new OrmDefinitionError(
|
|
557
|
-
`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`,
|
|
558
598
|
authored.file
|
|
559
599
|
);
|
|
560
600
|
}
|
|
@@ -878,9 +918,477 @@ function scalarColumnType(table, property, helper) {
|
|
|
878
918
|
}
|
|
879
919
|
return type;
|
|
880
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
|
|
881
1389
|
var CANONICAL_FORMAT = 2;
|
|
882
1390
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
883
|
-
var
|
|
1391
|
+
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
884
1392
|
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
885
1393
|
var COLUMN_TYPES = [
|
|
886
1394
|
"uuid",
|
|
@@ -896,14 +1404,14 @@ var COLUMN_TYPES = [
|
|
|
896
1404
|
];
|
|
897
1405
|
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
898
1406
|
var canonicalEnumSchema = zod.z.object({
|
|
899
|
-
name:
|
|
1407
|
+
name: identifier2,
|
|
900
1408
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
901
1409
|
});
|
|
902
1410
|
var canonicalColumnSchema = zod.z.object({
|
|
903
1411
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
904
1412
|
name: zod.z.string().min(1).max(63),
|
|
905
1413
|
columnName: columnIdentifier,
|
|
906
|
-
type: zod.z.union([columnTypeSchema, zod.z.object({ enum:
|
|
1414
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
|
|
907
1415
|
nullable: zod.z.boolean().default(true),
|
|
908
1416
|
unique: zod.z.boolean().default(false),
|
|
909
1417
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -919,7 +1427,14 @@ var canonicalColumnSchema = zod.z.object({
|
|
|
919
1427
|
/** Migration hints — same semantics as v1. */
|
|
920
1428
|
renamedFrom: columnIdentifier.optional(),
|
|
921
1429
|
cast: zod.z.string().max(200).optional(),
|
|
922
|
-
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()
|
|
923
1438
|
}).superRefine((column, ctx) => {
|
|
924
1439
|
if (column.onUpdateFn === void 0) return;
|
|
925
1440
|
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
@@ -934,36 +1449,26 @@ var canonicalLinkSchema = zod.z.object({
|
|
|
934
1449
|
name: zod.z.string().min(1).max(63),
|
|
935
1450
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
936
1451
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
937
|
-
target:
|
|
1452
|
+
target: identifier2.optional(),
|
|
938
1453
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
939
1454
|
on: columnIdentifier,
|
|
940
1455
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
941
1456
|
references: columnIdentifier.optional(),
|
|
942
1457
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
943
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(),
|
|
944
1461
|
description: zod.z.string().max(500).optional()
|
|
945
1462
|
});
|
|
946
1463
|
var canonicalIndexSchema = zod.z.object({
|
|
947
1464
|
columns: zod.z.array(columnIdentifier).min(1),
|
|
948
1465
|
unique: zod.z.boolean().default(false),
|
|
949
|
-
name:
|
|
1466
|
+
name: identifier2.optional()
|
|
950
1467
|
});
|
|
951
|
-
var
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
allow: zod.z.literal(true).optional(),
|
|
956
|
-
policy: zod.z.string().min(1).max(2e3).optional()
|
|
957
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
958
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
959
|
-
});
|
|
960
|
-
var policyExpressionSchema = zod.z.union([
|
|
961
|
-
zod.z.string().min(1).max(2e3),
|
|
962
|
-
zod.z.object({
|
|
963
|
-
default: zod.z.string().min(1).max(2e3),
|
|
964
|
-
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
965
|
-
})
|
|
966
|
-
]);
|
|
1468
|
+
var canonicalCheckSchema = zod.z.object({
|
|
1469
|
+
name: identifier2,
|
|
1470
|
+
expression: zod.z.string().min(1).max(2e3)
|
|
1471
|
+
}).strict();
|
|
967
1472
|
var canonicalPoliciesSchema = zod.z.object({
|
|
968
1473
|
read: policyExpressionSchema.optional(),
|
|
969
1474
|
write: policyExpressionSchema.optional(),
|
|
@@ -1151,7 +1656,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
|
1151
1656
|
"width_bucket"
|
|
1152
1657
|
];
|
|
1153
1658
|
var canonicalActionSchema = zod.z.object({
|
|
1154
|
-
name:
|
|
1659
|
+
name: identifier2,
|
|
1155
1660
|
requires: zod.z.string().max(1e3).optional(),
|
|
1156
1661
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
1157
1662
|
type: columnTypeSchema,
|
|
@@ -1163,9 +1668,9 @@ var canonicalActionSchema = zod.z.object({
|
|
|
1163
1668
|
});
|
|
1164
1669
|
var canonicalCowMutationSchema = zod.z.object({
|
|
1165
1670
|
kind: zod.z.enum(["override", "revert"]),
|
|
1166
|
-
name:
|
|
1671
|
+
name: identifier2,
|
|
1167
1672
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
1168
|
-
synced:
|
|
1673
|
+
synced: identifier2,
|
|
1169
1674
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
1170
1675
|
pk: zod.z.string().min(1).max(63),
|
|
1171
1676
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -1193,23 +1698,27 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
1193
1698
|
}
|
|
1194
1699
|
});
|
|
1195
1700
|
var canonicalForeignKeySchema = zod.z.object({
|
|
1196
|
-
name:
|
|
1701
|
+
name: identifier2,
|
|
1197
1702
|
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
1198
|
-
target:
|
|
1703
|
+
target: identifier2,
|
|
1199
1704
|
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
1200
|
-
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
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()
|
|
1201
1708
|
}).strict();
|
|
1202
1709
|
var canonicalTableSchema = zod.z.object({
|
|
1203
|
-
table:
|
|
1710
|
+
table: identifier2,
|
|
1204
1711
|
description: zod.z.string().max(2e3).optional(),
|
|
1205
1712
|
/** Table-level rename hint (new in v2). */
|
|
1206
|
-
renamedFrom:
|
|
1713
|
+
renamedFrom: identifier2.optional(),
|
|
1207
1714
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
1208
1715
|
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
1209
1716
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
1210
1717
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1211
1718
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1212
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(),
|
|
1213
1722
|
policies: canonicalPoliciesSchema.default({}),
|
|
1214
1723
|
/**
|
|
1215
1724
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -1220,22 +1729,55 @@ var canonicalTableSchema = zod.z.object({
|
|
|
1220
1729
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
1221
1730
|
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
1222
1731
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1223
|
-
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
|
|
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()
|
|
1224
1753
|
});
|
|
1754
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
1755
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
1225
1756
|
var canonicalViewSchema = zod.z.object({
|
|
1226
|
-
view:
|
|
1757
|
+
view: identifier2,
|
|
1227
1758
|
description: zod.z.string().max(2e3).optional(),
|
|
1228
1759
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
1229
1760
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
1230
1761
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1231
1762
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1232
1763
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
1233
|
-
query: zod.z.string().min(1).max(
|
|
1764
|
+
query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
1234
1765
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
1235
1766
|
}),
|
|
1236
1767
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
1237
|
-
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()
|
|
1238
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
|
+
}
|
|
1239
1781
|
for (const [index3, column] of view.columns.entries()) {
|
|
1240
1782
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1241
1783
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
@@ -1250,15 +1792,82 @@ var canonicalViewSchema = zod.z.object({
|
|
|
1250
1792
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
|
|
1251
1793
|
}
|
|
1252
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
|
+
}
|
|
1253
1800
|
for (const [index3, link] of view.links.entries()) {
|
|
1254
1801
|
if (link.enforce) {
|
|
1255
1802
|
ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
1256
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
|
+
}
|
|
1257
1807
|
}
|
|
1258
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
|
+
}
|
|
1259
1868
|
var canonicalDefinitionSchema = zod.z.object({
|
|
1260
1869
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
1261
|
-
namespace:
|
|
1870
|
+
namespace: identifier2.max(41),
|
|
1262
1871
|
description: zod.z.string().max(2e3).optional(),
|
|
1263
1872
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
1264
1873
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -1276,6 +1885,7 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1276
1885
|
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
1277
1886
|
}
|
|
1278
1887
|
seen.add(table.table);
|
|
1888
|
+
validateCellMetadata(table, index3, ctx);
|
|
1279
1889
|
if (table.source !== void 0) {
|
|
1280
1890
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1281
1891
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -1295,6 +1905,26 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1295
1905
|
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
1296
1906
|
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
1297
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
|
+
}
|
|
1298
1928
|
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
1299
1929
|
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
|
|
1300
1930
|
if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
|
|
@@ -1312,10 +1942,18 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1312
1942
|
if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
|
|
1313
1943
|
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
|
|
1314
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
|
+
}
|
|
1315
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])];
|
|
1316
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");
|
|
1317
1950
|
}
|
|
1318
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
|
+
}
|
|
1956
|
+
}
|
|
1319
1957
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1320
1958
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1321
1959
|
const cowMutations = table.cowMutations ?? [];
|
|
@@ -1439,8 +2077,12 @@ function canonicalRelations(definition) {
|
|
|
1439
2077
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
1440
2078
|
links: view.links,
|
|
1441
2079
|
indexes: [],
|
|
1442
|
-
|
|
1443
|
-
|
|
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 } : {}
|
|
1444
2086
|
}));
|
|
1445
2087
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
1446
2088
|
}
|
|
@@ -1450,13 +2092,14 @@ function normalizeCanonical(definition) {
|
|
|
1450
2092
|
...definition,
|
|
1451
2093
|
enums: sortByName(definition.enums),
|
|
1452
2094
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
1453
|
-
const { cowMutations, foreignKeys, ...rest } = t2;
|
|
2095
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t2;
|
|
1454
2096
|
return {
|
|
1455
2097
|
...rest,
|
|
1456
2098
|
links: sortByName(t2.links),
|
|
1457
2099
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
1458
2100
|
actions: sortByName(t2.actions),
|
|
1459
2101
|
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
2102
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
1460
2103
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
1461
2104
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
1462
2105
|
};
|
|
@@ -1471,6 +2114,454 @@ function normalizeCanonical(definition) {
|
|
|
1471
2114
|
}
|
|
1472
2115
|
return normalized;
|
|
1473
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
|
+
}
|
|
1474
2565
|
function stableJson(value) {
|
|
1475
2566
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1476
2567
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -1577,8 +2668,19 @@ function defineNamespace(input) {
|
|
|
1577
2668
|
return config;
|
|
1578
2669
|
}
|
|
1579
2670
|
function serializeNamespace(input) {
|
|
1580
|
-
const { config
|
|
1581
|
-
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
|
+
}
|
|
1582
2684
|
const enums = input.enums ?? [];
|
|
1583
2685
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
1584
2686
|
for (const table of tables) {
|
|
@@ -1680,6 +2782,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1680
2782
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
1681
2783
|
if (table.source !== void 0) candidate.source = table.source;
|
|
1682
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
|
+
}
|
|
1683
2788
|
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
1684
2789
|
throw new OrmDefinitionError(
|
|
1685
2790
|
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
@@ -1717,6 +2822,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1717
2822
|
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1718
2823
|
candidate.columns?.push(column);
|
|
1719
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
|
+
}
|
|
1720
2830
|
if (table.config.primaryKey !== void 0) {
|
|
1721
2831
|
if (columnLevelPk.length > 0) {
|
|
1722
2832
|
throw new OrmDefinitionError(
|
|
@@ -1730,6 +2840,24 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1730
2840
|
} else if (columnLevelPk.length > 0) {
|
|
1731
2841
|
candidate.primaryKey = columnLevelPk;
|
|
1732
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
|
+
}
|
|
1733
2861
|
if (table.config.foreignKeys?.length) {
|
|
1734
2862
|
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1735
2863
|
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
@@ -1742,10 +2870,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1742
2870
|
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1743
2871
|
target: target.tableName,
|
|
1744
2872
|
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1745
|
-
...fk.deferrable ? { deferrable: fk.deferrable } : {}
|
|
2873
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {},
|
|
2874
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
2875
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
1746
2876
|
};
|
|
1747
2877
|
});
|
|
1748
2878
|
}
|
|
2879
|
+
if (table.config.checks?.length) {
|
|
2880
|
+
candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
|
|
2881
|
+
}
|
|
1749
2882
|
for (const index3 of table.config.indexes ?? []) {
|
|
1750
2883
|
const entry = {
|
|
1751
2884
|
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1841,7 +2974,33 @@ function buildCanonicalView({
|
|
|
1841
2974
|
columns,
|
|
1842
2975
|
...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
|
|
1843
2976
|
links,
|
|
1844
|
-
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
|
|
1845
3004
|
};
|
|
1846
3005
|
}
|
|
1847
3006
|
function buildCanonicalLink({
|
|
@@ -1858,6 +3017,9 @@ function buildCanonicalLink({
|
|
|
1858
3017
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
1859
3018
|
return fail("sets enforce: true, which is only supported on toOne links");
|
|
1860
3019
|
}
|
|
3020
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
|
|
3021
|
+
return fail("sets a referential action without enforce: true");
|
|
3022
|
+
}
|
|
1861
3023
|
if (link.linkKind === "toPlatformUser") {
|
|
1862
3024
|
if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
|
|
1863
3025
|
return {
|
|
@@ -1886,6 +3048,8 @@ function buildCanonicalLink({
|
|
|
1886
3048
|
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1887
3049
|
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
1888
3050
|
enforce: link.enforce,
|
|
3051
|
+
...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
|
|
3052
|
+
...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
|
|
1889
3053
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1890
3054
|
};
|
|
1891
3055
|
}
|
|
@@ -2339,7 +3503,7 @@ var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
|
|
|
2339
3503
|
var IDENTIFIER_START = /[a-z_]/;
|
|
2340
3504
|
var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
|
|
2341
3505
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
2342
|
-
function
|
|
3506
|
+
function tokenize2(source) {
|
|
2343
3507
|
const tokens = [];
|
|
2344
3508
|
let position = 0;
|
|
2345
3509
|
while (position < source.length) {
|
|
@@ -2413,7 +3577,7 @@ function readString(source, start) {
|
|
|
2413
3577
|
}
|
|
2414
3578
|
throw new PredicateParseError("unterminated string literal", start);
|
|
2415
3579
|
}
|
|
2416
|
-
var
|
|
3580
|
+
var Parser2 = class {
|
|
2417
3581
|
constructor(tokens) {
|
|
2418
3582
|
this.tokens = tokens;
|
|
2419
3583
|
}
|
|
@@ -2545,7 +3709,7 @@ var Parser = class {
|
|
|
2545
3709
|
function parsePredicate(source) {
|
|
2546
3710
|
const trimmed = source.trim();
|
|
2547
3711
|
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
2548
|
-
return { source: trimmed, ast: new
|
|
3712
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
2549
3713
|
}
|
|
2550
3714
|
|
|
2551
3715
|
// src/schema/predicates.ts
|
|
@@ -2630,8 +3794,8 @@ var actionSchema = zod.z.strictObject({
|
|
|
2630
3794
|
}
|
|
2631
3795
|
}
|
|
2632
3796
|
});
|
|
2633
|
-
function extractNamedParams(
|
|
2634
|
-
const withoutStrings =
|
|
3797
|
+
function extractNamedParams(sql7) {
|
|
3798
|
+
const withoutStrings = sql7.replace(QUOTED_STRING_PATTERN, "''");
|
|
2635
3799
|
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
2636
3800
|
return [...new Set(names)];
|
|
2637
3801
|
}
|
|
@@ -2698,17 +3862,6 @@ var SQL_TYPE_NAMES = {
|
|
|
2698
3862
|
};
|
|
2699
3863
|
var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
|
|
2700
3864
|
|
|
2701
|
-
// src/compiler/sql.ts
|
|
2702
|
-
var CLAIMS_SETTING = "request.jwt.claims";
|
|
2703
|
-
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
2704
|
-
var ROLES_CLAIM_KEY = "roles";
|
|
2705
|
-
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
2706
|
-
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
2707
|
-
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
2708
|
-
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
2709
|
-
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
2710
|
-
var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
2711
|
-
|
|
2712
3865
|
// src/compiler/predicates.ts
|
|
2713
3866
|
var SQL_COMPARE_OPS = {
|
|
2714
3867
|
"=": "=",
|
|
@@ -2788,8 +3941,8 @@ var actionParamArg = (name) => `p_${name}`;
|
|
|
2788
3941
|
function compileActionStatements(action, renderParam = actionParamArg) {
|
|
2789
3942
|
return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
|
|
2790
3943
|
}
|
|
2791
|
-
function compileStatement(
|
|
2792
|
-
const compiled =
|
|
3944
|
+
function compileStatement(sql7, renderParam) {
|
|
3945
|
+
const compiled = sql7.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
|
|
2793
3946
|
if (claimKey !== void 0) {
|
|
2794
3947
|
if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
|
|
2795
3948
|
return claimExpression(claimKey);
|
|
@@ -2854,10 +4007,10 @@ function physicalColumn(logical, options) {
|
|
|
2854
4007
|
return options.columnNames?.[logical] ?? logical;
|
|
2855
4008
|
}
|
|
2856
4009
|
function compileCmp(op, column, value, options) {
|
|
2857
|
-
const
|
|
2858
|
-
const declared = options.columnTypes?.[
|
|
4010
|
+
const physical2 = physicalColumn(column, options);
|
|
4011
|
+
const declared = options.columnTypes?.[physical2] ?? options.columnTypes?.[column];
|
|
2859
4012
|
const isEnum = typeof declared === "object" && declared !== null;
|
|
2860
|
-
const col = isEnum ? `${quoteIdentifier(
|
|
4013
|
+
const col = isEnum ? `${quoteIdentifier(physical2)}::text` : quoteIdentifier(physical2);
|
|
2861
4014
|
const columnType = isEnum ? "text" : declared;
|
|
2862
4015
|
switch (op) {
|
|
2863
4016
|
case "eq":
|
|
@@ -2901,11 +4054,11 @@ function compileEquality(col, value, columnType, negated, options) {
|
|
|
2901
4054
|
function compileValue(value, columnType, _options) {
|
|
2902
4055
|
switch (value.kind) {
|
|
2903
4056
|
case "literal": {
|
|
2904
|
-
const
|
|
2905
|
-
if (
|
|
2906
|
-
if (typeof
|
|
2907
|
-
if (typeof
|
|
2908
|
-
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);
|
|
2909
4062
|
}
|
|
2910
4063
|
case "claim": {
|
|
2911
4064
|
const expression = claimExpression(value.key);
|
|
@@ -2954,8 +4107,8 @@ function actionPrepareChecks(definition) {
|
|
|
2954
4107
|
const order = actionParamOrder(action);
|
|
2955
4108
|
const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
|
|
2956
4109
|
const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
|
|
2957
|
-
compiled.forEach((
|
|
2958
|
-
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;
|
|
2959
4112
|
statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
|
|
2960
4113
|
});
|
|
2961
4114
|
}
|
|
@@ -3337,14 +4490,14 @@ function comparisonTypeIssue(left, right, columns) {
|
|
|
3337
4490
|
return null;
|
|
3338
4491
|
}
|
|
3339
4492
|
const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
|
|
3340
|
-
const
|
|
3341
|
-
if (!column || !
|
|
4493
|
+
const literal2 = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
|
|
4494
|
+
if (!column || !literal2 || literal2.value === null) return null;
|
|
3342
4495
|
const columnGroup = groupOf(column);
|
|
3343
4496
|
if (!columnGroup) return null;
|
|
3344
|
-
if (typeof
|
|
3345
|
-
const literalGroup = typeof
|
|
4497
|
+
if (typeof literal2.value === "string") return null;
|
|
4498
|
+
const literalGroup = typeof literal2.value === "number" ? "numeric" : "boolean";
|
|
3346
4499
|
if (literalGroup !== columnGroup) {
|
|
3347
|
-
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`;
|
|
3348
4501
|
}
|
|
3349
4502
|
return null;
|
|
3350
4503
|
}
|
|
@@ -3430,8 +4583,8 @@ function syncedViewStatements({
|
|
|
3430
4583
|
const backing = backingMap.get(table.table);
|
|
3431
4584
|
if (!backing) continue;
|
|
3432
4585
|
const viewName = `${schema}.${quoteIdentifier(table.table)}`;
|
|
3433
|
-
const
|
|
3434
|
-
const physicalTarget = `${quoteIdentifier(
|
|
4586
|
+
const physical2 = splitPhysicalRelation(backing.physicalRelation);
|
|
4587
|
+
const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
|
|
3435
4588
|
const columnTypes = mergeSyncedViewColumnTypes({
|
|
3436
4589
|
table,
|
|
3437
4590
|
canonicalColumnTypes: columnTypesByTable?.get(table.table),
|
|
@@ -3445,7 +4598,7 @@ function syncedViewStatements({
|
|
|
3445
4598
|
columnNames: columnNamesByTable?.get(table.table)
|
|
3446
4599
|
})}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
|
|
3447
4600
|
if (backingPrivileges === "grant") {
|
|
3448
|
-
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(
|
|
4601
|
+
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical2.schema)} TO ${owner}`);
|
|
3449
4602
|
statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
|
|
3450
4603
|
}
|
|
3451
4604
|
statements.push(
|
|
@@ -3466,10 +4619,10 @@ function mergeSyncedViewColumnTypes({
|
|
|
3466
4619
|
if (def?.type) fromTable[name] = def.type;
|
|
3467
4620
|
}
|
|
3468
4621
|
if (columnNames) {
|
|
3469
|
-
for (const [logical,
|
|
3470
|
-
const declared = fromTable[
|
|
4622
|
+
for (const [logical, physical2] of Object.entries(columnNames)) {
|
|
4623
|
+
const declared = fromTable[physical2] ?? fromTable[logical];
|
|
3471
4624
|
if (!declared) continue;
|
|
3472
|
-
fromTable[
|
|
4625
|
+
fromTable[physical2] = declared;
|
|
3473
4626
|
fromTable[logical] = declared;
|
|
3474
4627
|
}
|
|
3475
4628
|
}
|
|
@@ -3508,8 +4661,8 @@ function syncedBackingRevokeStatements({
|
|
|
3508
4661
|
const seen = /* @__PURE__ */ new Set();
|
|
3509
4662
|
const statements = [];
|
|
3510
4663
|
for (const backing of backings) {
|
|
3511
|
-
const
|
|
3512
|
-
const physicalTarget = `${quoteIdentifier(
|
|
4664
|
+
const physical2 = splitPhysicalRelation(backing.physicalRelation);
|
|
4665
|
+
const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
|
|
3513
4666
|
if (seen.has(physicalTarget)) continue;
|
|
3514
4667
|
seen.add(physicalTarget);
|
|
3515
4668
|
statements.push(`REVOKE SELECT ON ${physicalTarget} FROM ${owner}`);
|
|
@@ -3719,6 +4872,90 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
3719
4872
|
const tables = snapshot.tables ?? {};
|
|
3720
4873
|
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
3721
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
|
|
3722
4959
|
var FACTORIES2 = {
|
|
3723
4960
|
uuid: (name) => pgCore.uuid(name),
|
|
3724
4961
|
text: (name) => pgCore.text(name),
|
|
@@ -3831,18 +5068,18 @@ function rewriteSyncedSourceRelationsInViewQuery({
|
|
|
3831
5068
|
return rewritten;
|
|
3832
5069
|
});
|
|
3833
5070
|
}
|
|
3834
|
-
function transformSqlOutsideLiterals(
|
|
5071
|
+
function transformSqlOutsideLiterals(sql7, transform) {
|
|
3835
5072
|
const out = [];
|
|
3836
5073
|
let state = { kind: "normal" };
|
|
3837
5074
|
let i = 0;
|
|
3838
5075
|
let codeStart = 0;
|
|
3839
5076
|
const emitCode = (end) => {
|
|
3840
|
-
if (end > codeStart) out.push(transform(
|
|
5077
|
+
if (end > codeStart) out.push(transform(sql7.slice(codeStart, end)));
|
|
3841
5078
|
codeStart = end;
|
|
3842
5079
|
};
|
|
3843
|
-
while (i <
|
|
3844
|
-
const c =
|
|
3845
|
-
const next = i + 1 <
|
|
5080
|
+
while (i < sql7.length) {
|
|
5081
|
+
const c = sql7[i];
|
|
5082
|
+
const next = i + 1 < sql7.length ? sql7[i + 1] : "";
|
|
3846
5083
|
switch (state.kind) {
|
|
3847
5084
|
case "normal": {
|
|
3848
5085
|
if (c === "-" && next === "-") {
|
|
@@ -3862,7 +5099,7 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3862
5099
|
break;
|
|
3863
5100
|
}
|
|
3864
5101
|
if ((c === "E" || c === "e") && next === "'") {
|
|
3865
|
-
const prev = i > 0 ?
|
|
5102
|
+
const prev = i > 0 ? sql7[i - 1] : "";
|
|
3866
5103
|
if (!/[A-Za-z0-9_]/.test(prev)) {
|
|
3867
5104
|
emitCode(i);
|
|
3868
5105
|
state = { kind: "e-single" };
|
|
@@ -3881,9 +5118,9 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3881
5118
|
break;
|
|
3882
5119
|
}
|
|
3883
5120
|
if (c === "$") {
|
|
3884
|
-
const prev = i > 0 ?
|
|
5121
|
+
const prev = i > 0 ? sql7[i - 1] : "";
|
|
3885
5122
|
if (!/[A-Za-z0-9_$]/.test(prev)) {
|
|
3886
|
-
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(
|
|
5123
|
+
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql7.slice(i));
|
|
3887
5124
|
if (tagMatch) {
|
|
3888
5125
|
emitCode(i);
|
|
3889
5126
|
state = { kind: "dollar", tag: tagMatch[1] ?? "" };
|
|
@@ -3915,8 +5152,8 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3915
5152
|
break;
|
|
3916
5153
|
}
|
|
3917
5154
|
case "e-single": {
|
|
3918
|
-
if (c === "\\" && i + 1 <
|
|
3919
|
-
out.push(c,
|
|
5155
|
+
if (c === "\\" && i + 1 < sql7.length) {
|
|
5156
|
+
out.push(c, sql7[i + 1]);
|
|
3920
5157
|
i += 2;
|
|
3921
5158
|
break;
|
|
3922
5159
|
}
|
|
@@ -3938,7 +5175,7 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3938
5175
|
}
|
|
3939
5176
|
case "dollar": {
|
|
3940
5177
|
const closer = `$${state.tag}$`;
|
|
3941
|
-
if (
|
|
5178
|
+
if (sql7.startsWith(closer, i)) {
|
|
3942
5179
|
out.push(closer);
|
|
3943
5180
|
state = { kind: "normal" };
|
|
3944
5181
|
i += closer.length;
|
|
@@ -3982,15 +5219,15 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3982
5219
|
}
|
|
3983
5220
|
}
|
|
3984
5221
|
}
|
|
3985
|
-
emitCode(
|
|
5222
|
+
emitCode(sql7.length);
|
|
3986
5223
|
return out.join("");
|
|
3987
5224
|
}
|
|
3988
5225
|
function escapeRegExp(value) {
|
|
3989
5226
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3990
5227
|
}
|
|
3991
|
-
function sqlReferencesQualifiedRelation(
|
|
5228
|
+
function sqlReferencesQualifiedRelation(sql7, sourceSchema, sourceName) {
|
|
3992
5229
|
let referenced = false;
|
|
3993
|
-
transformSqlOutsideLiterals(
|
|
5230
|
+
transformSqlOutsideLiterals(sql7, (segment) => {
|
|
3994
5231
|
if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
|
|
3995
5232
|
return segment;
|
|
3996
5233
|
});
|
|
@@ -4001,16 +5238,18 @@ function qualifiedRelationPattern(sourceSchema, sourceName) {
|
|
|
4001
5238
|
const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
|
|
4002
5239
|
return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
|
|
4003
5240
|
}
|
|
4004
|
-
function replaceQualifiedRelation(
|
|
4005
|
-
return
|
|
5241
|
+
function replaceQualifiedRelation(sql7, sourceSchema, sourceName, boundSchema, boundTable) {
|
|
5242
|
+
return sql7.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
|
|
4006
5243
|
}
|
|
4007
5244
|
function generateSnapshotV2(definition, previousId3) {
|
|
4008
5245
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
4009
5246
|
const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
4010
5247
|
if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
|
|
4011
5248
|
const extended = snapshot;
|
|
5249
|
+
const refreshFunctions = materializedRefreshFunctions(definition);
|
|
4012
5250
|
extended._sequenceOrm = {
|
|
4013
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) } : {},
|
|
4014
5253
|
viewDependencies: Object.fromEntries(
|
|
4015
5254
|
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
4016
5255
|
)
|
|
@@ -4050,15 +5289,19 @@ function buildTable2(schema, table, built, enums, byName, introspection) {
|
|
|
4050
5289
|
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
4051
5290
|
...buildCompositePrimaryKey(table, primaryKeyColumns, self),
|
|
4052
5291
|
...buildIndexes2(table, self),
|
|
5292
|
+
...(table.checks ?? []).map((check) => pgCore.check(check.name, drizzleOrm.sql.raw(compileCheckExpression(check.expression, table.columns)))),
|
|
4053
5293
|
...buildForeignKeys2(table, self, built, byName),
|
|
4054
5294
|
...(table.foreignKeys ?? []).map((fk) => {
|
|
4055
5295
|
const target = fk.target === table.table ? self : built.get(fk.target);
|
|
4056
5296
|
if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
|
|
4057
|
-
|
|
5297
|
+
let constraint = pgCore.foreignKey({
|
|
4058
5298
|
name: fk.name,
|
|
4059
5299
|
columns: fk.columns.map((column) => self[column]),
|
|
4060
5300
|
foreignColumns: fk.references.map((column) => target[column])
|
|
4061
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;
|
|
4062
5305
|
})
|
|
4063
5306
|
]);
|
|
4064
5307
|
return drizzleTable.enableRLS();
|
|
@@ -4149,11 +5392,14 @@ function buildForeignKeys2(table, self, built, byName) {
|
|
|
4149
5392
|
`enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
|
|
4150
5393
|
);
|
|
4151
5394
|
}
|
|
4152
|
-
|
|
5395
|
+
let constraint = pgCore.foreignKey({
|
|
4153
5396
|
name: foreignKeyName(table.table, link.name),
|
|
4154
5397
|
columns: [sourceColumn],
|
|
4155
5398
|
foreignColumns: [targetColumn]
|
|
4156
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;
|
|
4157
5403
|
});
|
|
4158
5404
|
}
|
|
4159
5405
|
var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
|
|
@@ -4274,9 +5520,23 @@ function findTable2(snapshot, schema, table) {
|
|
|
4274
5520
|
const tables = snapshot.tables ?? {};
|
|
4275
5521
|
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
4276
5522
|
}
|
|
4277
|
-
|
|
5523
|
+
function planMigrationV2(input) {
|
|
5524
|
+
return planWithViewRecreation({ ...input, viewRecreation: "affected" });
|
|
5525
|
+
}
|
|
5526
|
+
async function planCommittedMigrationV2({
|
|
4278
5527
|
canonical,
|
|
4279
|
-
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
|
|
4280
5540
|
}) {
|
|
4281
5541
|
const schema = ormSchemaName(canonical.namespace);
|
|
4282
5542
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
@@ -4294,7 +5554,14 @@ async function planMigrationV2({
|
|
|
4294
5554
|
currentViews,
|
|
4295
5555
|
previousDependencies,
|
|
4296
5556
|
currentDependencies,
|
|
4297
|
-
|
|
5557
|
+
reach: viewReach({
|
|
5558
|
+
viewRecreation,
|
|
5559
|
+
hintStatements: hints.statements,
|
|
5560
|
+
baseDdl,
|
|
5561
|
+
previous,
|
|
5562
|
+
adjusted: hints.adjusted,
|
|
5563
|
+
current: snapshot
|
|
5564
|
+
})
|
|
4298
5565
|
});
|
|
4299
5566
|
const dropOrder = topologicalViewNames(
|
|
4300
5567
|
new Set([...affected].filter((name) => previousViews.has(name))),
|
|
@@ -4321,13 +5588,18 @@ async function planMigrationV2({
|
|
|
4321
5588
|
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
4322
5589
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
4323
5590
|
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
4324
|
-
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((
|
|
5591
|
+
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql7) => ({ kind: "ddl", sql: sql7 })),
|
|
4325
5592
|
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
4326
5593
|
...createOrder.map((name) => {
|
|
4327
5594
|
const view = currentViewDefinitions.get(name);
|
|
4328
5595
|
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
|
|
4329
5596
|
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
4330
|
-
})
|
|
5597
|
+
}),
|
|
5598
|
+
...refreshFunctionStatements({
|
|
5599
|
+
namespace: canonical.namespace,
|
|
5600
|
+
previous: previous._sequenceOrm?.refreshFunctions ?? {},
|
|
5601
|
+
current: materializedRefreshFunctions(canonical)
|
|
5602
|
+
}).map((sql7) => ({ kind: "ddl", sql: sql7 }))
|
|
4331
5603
|
];
|
|
4332
5604
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
4333
5605
|
}
|
|
@@ -4371,9 +5643,9 @@ function validateViewCatalogStatement({
|
|
|
4371
5643
|
}
|
|
4372
5644
|
const expectedDependencies = sqlTextArray(
|
|
4373
5645
|
view.dependencies.map((dependency) => {
|
|
4374
|
-
const
|
|
4375
|
-
if (!
|
|
4376
|
-
return
|
|
5646
|
+
const physical2 = relationIdentity.get(dependency);
|
|
5647
|
+
if (!physical2) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
5648
|
+
return physical2;
|
|
4377
5649
|
}).sort()
|
|
4378
5650
|
);
|
|
4379
5651
|
const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
@@ -4481,20 +5753,102 @@ function withoutViews(snapshot) {
|
|
|
4481
5753
|
delete clone._sequenceOrm;
|
|
4482
5754
|
return clone;
|
|
4483
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
|
+
}
|
|
4484
5836
|
function affectedViewNames({
|
|
4485
5837
|
previousViews,
|
|
4486
5838
|
currentViews,
|
|
4487
5839
|
previousDependencies,
|
|
4488
5840
|
currentDependencies,
|
|
4489
|
-
|
|
5841
|
+
reach
|
|
4490
5842
|
}) {
|
|
4491
5843
|
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
4492
|
-
if (
|
|
5844
|
+
if (reach === "all") return allNames;
|
|
5845
|
+
const dependenciesOf = (name) => /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
|
|
4493
5846
|
const affected = new Set(
|
|
4494
5847
|
[...allNames].filter((name) => {
|
|
4495
5848
|
const before = previousViews.get(name);
|
|
4496
5849
|
const after = currentViews.get(name);
|
|
4497
|
-
|
|
5850
|
+
if (before === void 0 || after === void 0 || stableJson(before) !== stableJson(after)) return true;
|
|
5851
|
+
return [...dependenciesOf(name)].some((dependency) => reach.has(dependency));
|
|
4498
5852
|
})
|
|
4499
5853
|
);
|
|
4500
5854
|
let grew = true;
|
|
@@ -4502,8 +5856,7 @@ function affectedViewNames({
|
|
|
4502
5856
|
grew = false;
|
|
4503
5857
|
for (const name of allNames) {
|
|
4504
5858
|
if (affected.has(name)) continue;
|
|
4505
|
-
|
|
4506
|
-
if ([...dependencies].some((dependency) => affected.has(dependency))) {
|
|
5859
|
+
if ([...dependenciesOf(name)].some((dependency) => affected.has(dependency))) {
|
|
4507
5860
|
affected.add(name);
|
|
4508
5861
|
grew = true;
|
|
4509
5862
|
}
|
|
@@ -4741,15 +6094,15 @@ async function planMigration({
|
|
|
4741
6094
|
const ddl = await diffStatements(hints.adjusted, snapshot);
|
|
4742
6095
|
const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
|
|
4743
6096
|
const statements = [
|
|
4744
|
-
...policyDrops.map((
|
|
4745
|
-
...hints.statements.map((
|
|
4746
|
-
...ddl.map((
|
|
4747
|
-
...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 })),
|
|
4748
6101
|
// PREPARE each action's executes first (plpgsql bodies don't check columns
|
|
4749
6102
|
// at CREATE), then create the functions. Both reference the tables the DDL
|
|
4750
6103
|
// above creates, so they close the plan.
|
|
4751
|
-
...actionPrepareChecks(definition).map((
|
|
4752
|
-
...actionFunctionStatements(definition).map((
|
|
6104
|
+
...actionPrepareChecks(definition).map((sql7) => ({ kind: "function", sql: sql7 })),
|
|
6105
|
+
...actionFunctionStatements(definition).map((sql7) => ({ kind: "function", sql: sql7 }))
|
|
4753
6106
|
];
|
|
4754
6107
|
return { namespace: definition.namespace, schema, statements, snapshot };
|
|
4755
6108
|
}
|
|
@@ -4879,6 +6232,21 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
4879
6232
|
() => zod.z.object({
|
|
4880
6233
|
typename: zod.z.string(),
|
|
4881
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(),
|
|
4882
6250
|
relations: zod.z.array(zod.z.object({
|
|
4883
6251
|
alias: zod.z.string(),
|
|
4884
6252
|
link: zod.z.string(),
|
|
@@ -4900,6 +6268,11 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
4900
6268
|
})
|
|
4901
6269
|
);
|
|
4902
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
|
+
});
|
|
4903
6276
|
var selectRootSchema = zod.z.object({
|
|
4904
6277
|
kind: zod.z.literal("select"),
|
|
4905
6278
|
alias: zod.z.string(),
|
|
@@ -4911,7 +6284,8 @@ var selectRootSchema = zod.z.object({
|
|
|
4911
6284
|
distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
|
|
4912
6285
|
limit: limitIrSchema,
|
|
4913
6286
|
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
4914
|
-
includeIf: includeIfSchema.optional()
|
|
6287
|
+
includeIf: includeIfSchema.optional(),
|
|
6288
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
4915
6289
|
});
|
|
4916
6290
|
var byPkRootSchema = zod.z.object({
|
|
4917
6291
|
kind: zod.z.literal("byPk"),
|
|
@@ -4921,7 +6295,8 @@ var byPkRootSchema = zod.z.object({
|
|
|
4921
6295
|
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
|
|
4922
6296
|
/** Policy predicate — ANDed with the PK match. */
|
|
4923
6297
|
where: predIrSchema,
|
|
4924
|
-
includeIf: includeIfSchema.optional()
|
|
6298
|
+
includeIf: includeIfSchema.optional(),
|
|
6299
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
4925
6300
|
});
|
|
4926
6301
|
var aggregateRootSchema = zod.z.object({
|
|
4927
6302
|
kind: zod.z.literal("aggregate"),
|
|
@@ -4979,7 +6354,20 @@ var mutationRootSchema = zod.z.object({
|
|
|
4979
6354
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
4980
6355
|
* row and carry the write policy in `where` like update/delete.
|
|
4981
6356
|
*/
|
|
4982
|
-
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(),
|
|
4983
6371
|
alias: zod.z.string(),
|
|
4984
6372
|
table: zod.z.string(),
|
|
4985
6373
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -5064,6 +6452,43 @@ var compiledOperationSchema = zod.z.object({
|
|
|
5064
6452
|
}
|
|
5065
6453
|
}
|
|
5066
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()
|
|
6491
|
+
});
|
|
5067
6492
|
var tableMetaIrSchema = zod.z.object({
|
|
5068
6493
|
table: zod.z.string(),
|
|
5069
6494
|
schema: zod.z.string(),
|
|
@@ -5104,6 +6529,22 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
5104
6529
|
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
5105
6530
|
*/
|
|
5106
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(),
|
|
5107
6548
|
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
5108
6549
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
5109
6550
|
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
@@ -5448,12 +6889,12 @@ async function writeMigration({
|
|
|
5448
6889
|
const tip = workspace.journal.entries.at(-1);
|
|
5449
6890
|
const ordinal = (tip?.ordinal ?? 0) + 1;
|
|
5450
6891
|
const name = migrationName(ordinal, contentHash);
|
|
5451
|
-
const
|
|
6892
|
+
const sql7 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
|
|
5452
6893
|
const metaDir = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
|
|
5453
6894
|
await promises.mkdir(metaDir, { recursive: true });
|
|
5454
6895
|
const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
|
|
5455
6896
|
`;
|
|
5456
|
-
await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`),
|
|
6897
|
+
await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql7);
|
|
5457
6898
|
await promises.writeFile(path.join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
|
|
5458
6899
|
const journal = {
|
|
5459
6900
|
...workspace.journal,
|
|
@@ -5464,7 +6905,7 @@ async function writeMigration({
|
|
|
5464
6905
|
name,
|
|
5465
6906
|
content_hash: contentHash,
|
|
5466
6907
|
previous_content_hash: tip?.content_hash ?? null,
|
|
5467
|
-
checksum: checksumOf(
|
|
6908
|
+
checksum: checksumOf(sql7),
|
|
5468
6909
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5469
6910
|
destructive
|
|
5470
6911
|
}
|
|
@@ -5579,8 +7020,8 @@ async function checkWorkspace({
|
|
|
5579
7020
|
let previousId3;
|
|
5580
7021
|
let previousHash = null;
|
|
5581
7022
|
for (const entry of workspace.journal.entries) {
|
|
5582
|
-
const
|
|
5583
|
-
if (checksumOf(
|
|
7023
|
+
const sql7 = await readMigrationSql(dir, entry);
|
|
7024
|
+
if (checksumOf(sql7) !== entry.checksum) {
|
|
5584
7025
|
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
5585
7026
|
return 1;
|
|
5586
7027
|
}
|
|
@@ -5620,7 +7061,7 @@ async function checkWorkspace({
|
|
|
5620
7061
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
5621
7062
|
return 0;
|
|
5622
7063
|
}
|
|
5623
|
-
var firstLine = (
|
|
7064
|
+
var firstLine = (sql7) => sql7.split("\n")[0] ?? sql7;
|
|
5624
7065
|
|
|
5625
7066
|
// src/migrations/from-yaml.ts
|
|
5626
7067
|
function convertV1DefinitionToCanonical(definition) {
|
|
@@ -5726,7 +7167,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5726
7167
|
throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
|
|
5727
7168
|
}
|
|
5728
7169
|
const name = migrationName(ordinal, newHash);
|
|
5729
|
-
const
|
|
7170
|
+
const sql7 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
|
|
5730
7171
|
const snapshotText = `${JSON.stringify(snapshot, null, 2)}
|
|
5731
7172
|
`;
|
|
5732
7173
|
const entry = {
|
|
@@ -5734,11 +7175,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5734
7175
|
name,
|
|
5735
7176
|
content_hash: newHash,
|
|
5736
7177
|
previous_content_hash: previousHash,
|
|
5737
|
-
checksum: checksumOf(
|
|
7178
|
+
checksum: checksumOf(sql7),
|
|
5738
7179
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5739
7180
|
destructive: false
|
|
5740
7181
|
};
|
|
5741
|
-
return { name, entry, sql:
|
|
7182
|
+
return { name, entry, sql: sql7, snapshotText };
|
|
5742
7183
|
}
|
|
5743
7184
|
function renderRebaseMigrationSql({
|
|
5744
7185
|
name,
|
|
@@ -5862,6 +7303,9 @@ exports.AUDIT_ACTIONS = AUDIT_ACTIONS;
|
|
|
5862
7303
|
exports.CANONICAL_FORMAT = CANONICAL_FORMAT;
|
|
5863
7304
|
exports.CAPABILITIES_CLAIM_KEY = CAPABILITIES_CLAIM_KEY;
|
|
5864
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;
|
|
5865
7309
|
exports.CLAIMS_CONTRACT = CLAIMS_CONTRACT;
|
|
5866
7310
|
exports.CLAIMS_SETTING = CLAIMS_SETTING;
|
|
5867
7311
|
exports.CLAIM_KEY_PATTERN = CLAIM_KEY_PATTERN;
|
|
@@ -5884,9 +7328,12 @@ exports.IR_VERSION = IR_VERSION;
|
|
|
5884
7328
|
exports.IrVersionError = IrVersionError;
|
|
5885
7329
|
exports.LEGACY_IR_VERSION = LEGACY_IR_VERSION;
|
|
5886
7330
|
exports.LINK_BRAND = LINK_BRAND;
|
|
7331
|
+
exports.MATERIALIZE_ACTOR = MATERIALIZE_ACTOR;
|
|
7332
|
+
exports.MATERIALIZE_CLAIMS_JSON = MATERIALIZE_CLAIMS_JSON;
|
|
5887
7333
|
exports.MAX_DEFINITION_FILE_BYTES = MAX_DEFINITION_FILE_BYTES;
|
|
5888
7334
|
exports.MAX_GENERATED_SDL_BYTES = MAX_GENERATED_SDL_BYTES;
|
|
5889
7335
|
exports.MAX_IDENTIFIER_LENGTH = MAX_IDENTIFIER_LENGTH;
|
|
7336
|
+
exports.MAX_TRACKED_COLUMN_NAME = MAX_TRACKED_COLUMN_NAME;
|
|
5890
7337
|
exports.MIGRATIONS_DIRNAME = MIGRATIONS_DIRNAME;
|
|
5891
7338
|
exports.NAMESPACE_CONFIG_BRAND = NAMESPACE_CONFIG_BRAND;
|
|
5892
7339
|
exports.NAMESPACE_CONFIG_FILE = NAMESPACE_CONFIG_FILE;
|
|
@@ -5943,6 +7390,7 @@ exports.canonicalViewSchema = canonicalViewSchema;
|
|
|
5943
7390
|
exports.canonicalizeDefinition = canonicalizeDefinition;
|
|
5944
7391
|
exports.capabilityExpression = capabilityExpression;
|
|
5945
7392
|
exports.castParts = castParts;
|
|
7393
|
+
exports.cellColumnName = cellColumnName;
|
|
5946
7394
|
exports.checksumOf = checksumOf;
|
|
5947
7395
|
exports.claimExpression = claimExpression;
|
|
5948
7396
|
exports.classifyDestructive = classifyDestructive;
|
|
@@ -5961,9 +7409,11 @@ exports.computeSyncedContractHash = computeSyncedContractHash;
|
|
|
5961
7409
|
exports.consumerRoleName = consumerRoleName;
|
|
5962
7410
|
exports.convertV1DefinitionToCanonical = convertV1DefinitionToCanonical;
|
|
5963
7411
|
exports.createOrmMigrationExport = createOrmMigrationExport;
|
|
7412
|
+
exports.custom = custom;
|
|
5964
7413
|
exports.declaredCapabilityKeys = declaredCapabilityKeys;
|
|
5965
7414
|
exports.defineEnum = defineEnum;
|
|
5966
7415
|
exports.defineNamespace = defineNamespace;
|
|
7416
|
+
exports.defineResolvedTable = defineResolvedTable;
|
|
5967
7417
|
exports.defineSyncedTable = defineSyncedTable;
|
|
5968
7418
|
exports.defineTable = defineTable;
|
|
5969
7419
|
exports.defineView = defineView;
|
|
@@ -5973,11 +7423,13 @@ exports.diffStatements = diffStatements;
|
|
|
5973
7423
|
exports.emptyJournal = emptyJournal;
|
|
5974
7424
|
exports.emptySnapshot = emptySnapshot;
|
|
5975
7425
|
exports.endAuthoringContext = endAuthoringContext;
|
|
7426
|
+
exports.etlSource = etlSource;
|
|
5976
7427
|
exports.extractNamedParams = extractNamedParams;
|
|
5977
7428
|
exports.extractOrmSyncedTableContract = extractOrmSyncedTableContract;
|
|
5978
7429
|
exports.generateNamespaceTypes = generateNamespaceTypes;
|
|
5979
7430
|
exports.generateSnapshot = generateSnapshot;
|
|
5980
7431
|
exports.generateSnapshotV2 = generateSnapshotV2;
|
|
7432
|
+
exports.highest = highest;
|
|
5981
7433
|
exports.identifierSchema = identifierSchema;
|
|
5982
7434
|
exports.indexByHash = indexByHash;
|
|
5983
7435
|
exports.isColumnBuilder = isColumnBuilder;
|
|
@@ -5990,14 +7442,21 @@ exports.isSeqLink = isSeqLink;
|
|
|
5990
7442
|
exports.isSeqNamespaceConfig = isSeqNamespaceConfig;
|
|
5991
7443
|
exports.isSeqTable = isSeqTable;
|
|
5992
7444
|
exports.isSeqView = isSeqView;
|
|
7445
|
+
exports.latest = latest;
|
|
5993
7446
|
exports.limitIrSchema = limitIrSchema;
|
|
5994
7447
|
exports.loadCommittedMigrations = loadCommittedMigrations;
|
|
5995
7448
|
exports.loadCommittedTipMigration = loadCommittedTipMigration;
|
|
5996
7449
|
exports.loadNamespaceDefinition = loadNamespaceDefinition;
|
|
5997
7450
|
exports.loadNamespaceFromDir = loadNamespaceFromDir;
|
|
7451
|
+
exports.lowest = lowest;
|
|
7452
|
+
exports.managedFn = managedFn;
|
|
7453
|
+
exports.manualSource = manualSource;
|
|
7454
|
+
exports.materializeLockKey = materializeLockKey;
|
|
7455
|
+
exports.materializedRefreshFunctionName = materializedRefreshFunctionName;
|
|
5998
7456
|
exports.migrationName = migrationName;
|
|
5999
7457
|
exports.namespaceConfigSchema = namespaceConfigSchema;
|
|
6000
7458
|
exports.namespaceSchema = namespaceSchema;
|
|
7459
|
+
exports.nonNullFirst = nonNullFirst;
|
|
6001
7460
|
exports.normalizeCanonical = normalizeCanonical;
|
|
6002
7461
|
exports.normalizeIr = normalizeIr;
|
|
6003
7462
|
exports.normalizeSnapshot = normalizeSnapshot;
|
|
@@ -6014,12 +7473,16 @@ exports.parseTableFile = parseTableFile;
|
|
|
6014
7473
|
exports.parsedPredicateSchema = parsedPredicateSchema;
|
|
6015
7474
|
exports.pendingKitStatements = pendingKitStatements;
|
|
6016
7475
|
exports.pendingKitStatementsV2 = pendingKitStatementsV2;
|
|
7476
|
+
exports.planCommittedMigrationV2 = planCommittedMigrationV2;
|
|
6017
7477
|
exports.planMigration = planMigration;
|
|
6018
7478
|
exports.planMigrationV2 = planMigrationV2;
|
|
6019
7479
|
exports.policyExpressionSchema = policyExpressionSchema;
|
|
6020
7480
|
exports.policyVariantSchema = policyVariantSchema;
|
|
6021
7481
|
exports.predIrSchema = predIrSchema;
|
|
6022
7482
|
exports.predicateOperands = predicateOperands;
|
|
7483
|
+
exports.prefetchPlanIrSchema = prefetchPlanIrSchema;
|
|
7484
|
+
exports.prefetchSource = prefetchSource;
|
|
7485
|
+
exports.priority = priority;
|
|
6023
7486
|
exports.quoteIdentifier = quoteIdentifier;
|
|
6024
7487
|
exports.quoteLiteral = quoteLiteral;
|
|
6025
7488
|
exports.readMigrationSql = readMigrationSql;
|
|
@@ -6031,6 +7494,8 @@ exports.referencedColumns = referencedColumns;
|
|
|
6031
7494
|
exports.renderMigrationSql = renderMigrationSql;
|
|
6032
7495
|
exports.renderOrmMigrationExport = renderOrmMigrationExport;
|
|
6033
7496
|
exports.requiredArtifactIrVersion = requiredArtifactIrVersion;
|
|
7497
|
+
exports.resolvedMetaIrSchema = resolvedMetaIrSchema;
|
|
7498
|
+
exports.resolvedSourceIrSchema = resolvedSourceIrSchema;
|
|
6034
7499
|
exports.revertOverlayMutation = revertOverlayMutation;
|
|
6035
7500
|
exports.rewriteSyncedSourceRelationsInViewQuery = rewriteSyncedSourceRelationsInViewQuery;
|
|
6036
7501
|
exports.roleExpression = roleExpression;
|