@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.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sql, eq, notExists, is, SQL } from 'drizzle-orm';
|
|
2
|
-
import { pgSchema, uniqueIndex, index, foreignKey, primaryKey, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
2
|
+
import { pgSchema, uniqueIndex, index, foreignKey, check, primaryKey, getTableConfig, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
|
|
4
3
|
import { z } from 'zod';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
5
|
import { createJiti } from 'jiti';
|
|
6
6
|
import { realpath, stat, readdir, readFile, mkdir, writeFile } from 'fs/promises';
|
|
7
7
|
import { createRequire } from 'module';
|
|
@@ -38,7 +38,8 @@ var ColumnBuilder = class {
|
|
|
38
38
|
description: void 0,
|
|
39
39
|
renamedFrom: void 0,
|
|
40
40
|
cast: void 0,
|
|
41
|
-
deprecated: false
|
|
41
|
+
deprecated: false,
|
|
42
|
+
cell: void 0
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
/** Marks this column as (part of) the primary key. Implies NOT NULL. */
|
|
@@ -98,6 +99,11 @@ var ColumnBuilder = class {
|
|
|
98
99
|
this.state.deprecated = true;
|
|
99
100
|
return this;
|
|
100
101
|
}
|
|
102
|
+
/** @internal System cell-metadata part of logical column `of` (generated resolved views only). */
|
|
103
|
+
cellPart(of, part) {
|
|
104
|
+
this.state.cell = { of, part };
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
101
107
|
/** Whether `.primaryKey()` was called — collected into the table-level PK. */
|
|
102
108
|
get isPrimaryKey() {
|
|
103
109
|
return this.state.primaryKey;
|
|
@@ -126,6 +132,7 @@ var ColumnBuilder = class {
|
|
|
126
132
|
if (this.state.description !== void 0) column.description = this.state.description;
|
|
127
133
|
if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
|
|
128
134
|
if (this.state.cast !== void 0) column.cast = this.state.cast;
|
|
135
|
+
if (this.state.cell !== void 0) column.cell = { ...this.state.cell };
|
|
129
136
|
return column;
|
|
130
137
|
}
|
|
131
138
|
};
|
|
@@ -149,12 +156,12 @@ var t = {
|
|
|
149
156
|
|
|
150
157
|
// src/constants.ts
|
|
151
158
|
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
159
|
+
var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
|
|
160
|
+
var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
|
|
152
161
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
153
162
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
154
163
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
155
|
-
var
|
|
156
|
-
var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
|
|
157
|
-
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
164
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
158
165
|
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
159
166
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
160
167
|
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
@@ -170,6 +177,27 @@ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
|
|
|
170
177
|
var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
|
|
171
178
|
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
172
179
|
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
180
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
181
|
+
var CELL_PART_SUFFIX = {
|
|
182
|
+
at: "__at",
|
|
183
|
+
by: "__by",
|
|
184
|
+
rev: "__rev",
|
|
185
|
+
source: "__src",
|
|
186
|
+
conflicts: "__lost"
|
|
187
|
+
};
|
|
188
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
189
|
+
var CELLS_FIELD = "cells";
|
|
190
|
+
function cellColumnName(column, part) {
|
|
191
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
192
|
+
}
|
|
193
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
194
|
+
var MATERIALIZE_CLAIMS_JSON = JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
195
|
+
function materializedRefreshFunctionName(table) {
|
|
196
|
+
return `${table}__refresh`;
|
|
197
|
+
}
|
|
198
|
+
function materializeLockKey(schema, table) {
|
|
199
|
+
return `orm:materialize:${schema}.${table}`;
|
|
200
|
+
}
|
|
173
201
|
|
|
174
202
|
// src/errors.ts
|
|
175
203
|
var OrmDefinitionError = class extends Error {
|
|
@@ -245,6 +273,8 @@ function toOne(target, options) {
|
|
|
245
273
|
on: options.on,
|
|
246
274
|
references: options.references,
|
|
247
275
|
enforce: options.enforce ?? false,
|
|
276
|
+
onDelete: options.onDelete,
|
|
277
|
+
onUpdate: options.onUpdate,
|
|
248
278
|
description: options.description
|
|
249
279
|
};
|
|
250
280
|
}
|
|
@@ -256,6 +286,8 @@ function toMany(target, options) {
|
|
|
256
286
|
on: options.on,
|
|
257
287
|
references: options.references,
|
|
258
288
|
enforce: false,
|
|
289
|
+
onDelete: void 0,
|
|
290
|
+
onUpdate: void 0,
|
|
259
291
|
description: options.description
|
|
260
292
|
};
|
|
261
293
|
}
|
|
@@ -267,6 +299,8 @@ function toPlatformUser(options) {
|
|
|
267
299
|
on: options.on,
|
|
268
300
|
references: options.references,
|
|
269
301
|
enforce: false,
|
|
302
|
+
onDelete: void 0,
|
|
303
|
+
onUpdate: void 0,
|
|
270
304
|
description: options.description
|
|
271
305
|
};
|
|
272
306
|
}
|
|
@@ -301,6 +335,12 @@ function defineTable(name, columns, config = {}) {
|
|
|
301
335
|
}
|
|
302
336
|
const columnName = builder.columnNameFor(propertyName);
|
|
303
337
|
assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
|
|
338
|
+
if (config.materializes === void 0 && (propertyName === CELLS_FIELD || Object.values(CELL_PART_SUFFIX).some((suffix) => columnName.endsWith(suffix) || propertyName.endsWith(suffix)))) {
|
|
339
|
+
throw new OrmDefinitionError(
|
|
340
|
+
`table "${name}": "${propertyName}" collides with the reserved cell-metadata names ('${CELLS_FIELD}', or a ${Object.values(CELL_PART_SUFFIX).join("/")} suffix)`,
|
|
341
|
+
file
|
|
342
|
+
);
|
|
343
|
+
}
|
|
304
344
|
const existing = seenColumnNames.get(columnName);
|
|
305
345
|
if (existing !== void 0) {
|
|
306
346
|
throw new OrmDefinitionError(
|
|
@@ -548,11 +588,11 @@ function compileViewQueries(input) {
|
|
|
548
588
|
);
|
|
549
589
|
}
|
|
550
590
|
if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
|
|
551
|
-
const tableDependencies = extractUsedTable(config.query).map((
|
|
552
|
-
const logical = physicalToLogical.get(
|
|
591
|
+
const tableDependencies = extractUsedTable(config.query).map((physical2) => {
|
|
592
|
+
const logical = physicalToLogical.get(physical2);
|
|
553
593
|
if (!logical) {
|
|
554
594
|
throw new OrmDefinitionError(
|
|
555
|
-
`view "${authored.viewName}" query references unmanaged relation "${
|
|
595
|
+
`view "${authored.viewName}" query references unmanaged relation "${physical2}" \u2014 use relation(...) with a namespace table or view`,
|
|
556
596
|
authored.file
|
|
557
597
|
);
|
|
558
598
|
}
|
|
@@ -876,9 +916,477 @@ function scalarColumnType(table, property, helper) {
|
|
|
876
916
|
}
|
|
877
917
|
return type;
|
|
878
918
|
}
|
|
919
|
+
|
|
920
|
+
// src/compiler/sql.ts
|
|
921
|
+
var CLAIMS_SETTING = "request.jwt.claims";
|
|
922
|
+
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
923
|
+
var ROLES_CLAIM_KEY = "roles";
|
|
924
|
+
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
925
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
926
|
+
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
927
|
+
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
928
|
+
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
929
|
+
var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
930
|
+
|
|
931
|
+
// src/checks/compile.ts
|
|
932
|
+
function tokenize(source) {
|
|
933
|
+
const tokens = [];
|
|
934
|
+
for (let position = 0; position < source.length; ) {
|
|
935
|
+
const char = source[position];
|
|
936
|
+
if (/\s/.test(char)) {
|
|
937
|
+
position++;
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
941
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
942
|
+
position++;
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
if (char === "'") {
|
|
946
|
+
const start = position++;
|
|
947
|
+
let value = "";
|
|
948
|
+
let closed = false;
|
|
949
|
+
while (position < source.length) {
|
|
950
|
+
if (source[position] !== "'") {
|
|
951
|
+
value += source[position++];
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (source[position + 1] === "'") {
|
|
955
|
+
value += "'";
|
|
956
|
+
position += 2;
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
position++;
|
|
960
|
+
closed = true;
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
964
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
965
|
+
tokens.push({ kind: "string", value, position: start });
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
969
|
+
if (operator) {
|
|
970
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
971
|
+
position += operator[0].length;
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
975
|
+
if (number) {
|
|
976
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
977
|
+
position += number[0].length;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
981
|
+
if (identifier3) {
|
|
982
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
983
|
+
position += identifier3[0].length;
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
987
|
+
}
|
|
988
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
989
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
990
|
+
return tokens;
|
|
991
|
+
}
|
|
992
|
+
var Parser = class {
|
|
993
|
+
constructor(tokens, columns) {
|
|
994
|
+
this.tokens = tokens;
|
|
995
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
996
|
+
for (const column of columns) {
|
|
997
|
+
for (const alias of [column.name, column.columnName]) {
|
|
998
|
+
const previous = this.columns.get(alias);
|
|
999
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
1000
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
1001
|
+
}
|
|
1002
|
+
this.columns.set(alias, column);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
index = 0;
|
|
1007
|
+
depth = 0;
|
|
1008
|
+
columns;
|
|
1009
|
+
parse() {
|
|
1010
|
+
const expression = this.parseOr();
|
|
1011
|
+
this.expect("end", "end of expression");
|
|
1012
|
+
return expression;
|
|
1013
|
+
}
|
|
1014
|
+
parseOr() {
|
|
1015
|
+
let left = this.parseAnd();
|
|
1016
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
1017
|
+
return left;
|
|
1018
|
+
}
|
|
1019
|
+
parseAnd() {
|
|
1020
|
+
let left = this.parseNot();
|
|
1021
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
1022
|
+
return left;
|
|
1023
|
+
}
|
|
1024
|
+
parseNot() {
|
|
1025
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
1026
|
+
return this.parsePrimary();
|
|
1027
|
+
}
|
|
1028
|
+
parsePrimary() {
|
|
1029
|
+
if (this.peek().kind === "lparen") {
|
|
1030
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
1031
|
+
this.advance();
|
|
1032
|
+
const expression = this.parseOr();
|
|
1033
|
+
this.expect("rparen", "')'");
|
|
1034
|
+
this.depth--;
|
|
1035
|
+
return `(${expression})`;
|
|
1036
|
+
}
|
|
1037
|
+
return this.parseComparison();
|
|
1038
|
+
}
|
|
1039
|
+
parseComparison() {
|
|
1040
|
+
const left = this.parseValue();
|
|
1041
|
+
if (this.matchKeyword("is")) {
|
|
1042
|
+
const negate = this.matchKeyword("not");
|
|
1043
|
+
this.expectKeyword("null");
|
|
1044
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
1045
|
+
}
|
|
1046
|
+
if (this.matchKeyword("between")) {
|
|
1047
|
+
const lower = this.parseValue();
|
|
1048
|
+
this.expectKeyword("and");
|
|
1049
|
+
const upper = this.parseValue();
|
|
1050
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
1051
|
+
}
|
|
1052
|
+
if (this.matchKeyword("in")) {
|
|
1053
|
+
this.expect("lparen", "'(' after IN");
|
|
1054
|
+
const values = [this.parseValue()];
|
|
1055
|
+
while (this.peek().kind === "comma") {
|
|
1056
|
+
this.advance();
|
|
1057
|
+
values.push(this.parseValue());
|
|
1058
|
+
}
|
|
1059
|
+
this.expect("rparen", "')' after IN values");
|
|
1060
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
1061
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
1062
|
+
}
|
|
1063
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
1064
|
+
const right = this.parseValue();
|
|
1065
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
1066
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
1067
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
1068
|
+
}
|
|
1069
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
1070
|
+
}
|
|
1071
|
+
parseValue() {
|
|
1072
|
+
const token = this.advance();
|
|
1073
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
1074
|
+
if (token.kind === "number") {
|
|
1075
|
+
const number = Number(token.value);
|
|
1076
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
1077
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
1078
|
+
}
|
|
1079
|
+
return { sql: token.value, literal: true };
|
|
1080
|
+
}
|
|
1081
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
1082
|
+
const keyword = token.value.toLowerCase();
|
|
1083
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
1084
|
+
if (keyword === "btrim") {
|
|
1085
|
+
this.expect("lparen", "'(' after btrim");
|
|
1086
|
+
const column2 = this.parseColumn();
|
|
1087
|
+
this.expect("rparen", "')' after btrim column");
|
|
1088
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
1089
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
1090
|
+
}
|
|
1091
|
+
const column = this.columns.get(token.value);
|
|
1092
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
1093
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
1094
|
+
}
|
|
1095
|
+
parseColumn() {
|
|
1096
|
+
const token = this.expect("identifier", "a column name");
|
|
1097
|
+
const column = this.columns.get(token.value);
|
|
1098
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
1099
|
+
return column;
|
|
1100
|
+
}
|
|
1101
|
+
peek() {
|
|
1102
|
+
return this.tokens[this.index];
|
|
1103
|
+
}
|
|
1104
|
+
advance() {
|
|
1105
|
+
const token = this.peek();
|
|
1106
|
+
if (token.kind !== "end") this.index++;
|
|
1107
|
+
return token;
|
|
1108
|
+
}
|
|
1109
|
+
matchKeyword(keyword) {
|
|
1110
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
1111
|
+
this.advance();
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
expectKeyword(keyword) {
|
|
1115
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
1116
|
+
}
|
|
1117
|
+
expect(kind, description) {
|
|
1118
|
+
const token = this.peek();
|
|
1119
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
1120
|
+
return this.advance();
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
function compileCheckExpression(source, columns) {
|
|
1124
|
+
return new Parser(tokenize(source), columns).parse();
|
|
1125
|
+
}
|
|
1126
|
+
var policyVariantSchema = z.object({
|
|
1127
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
1128
|
+
when: z.string().min(1).max(1e3),
|
|
1129
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
1130
|
+
allow: z.literal(true).optional(),
|
|
1131
|
+
policy: z.string().min(1).max(2e3).optional()
|
|
1132
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
1133
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
1134
|
+
});
|
|
1135
|
+
var policyExpressionSchema = z.union([
|
|
1136
|
+
z.string().min(1).max(2e3),
|
|
1137
|
+
z.object({
|
|
1138
|
+
default: z.string().min(1).max(2e3),
|
|
1139
|
+
variants: z.array(policyVariantSchema).max(10).default([])
|
|
1140
|
+
})
|
|
1141
|
+
]);
|
|
1142
|
+
function parseDurationMs(input) {
|
|
1143
|
+
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
1144
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
1145
|
+
if (!match) return void 0;
|
|
1146
|
+
const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
|
|
1147
|
+
const value = Math.round(Number(match[1]) * factor);
|
|
1148
|
+
return value > 0 ? value : void 0;
|
|
1149
|
+
}
|
|
1150
|
+
var identifier = z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
1151
|
+
var durationMs = z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
1152
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
1153
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
1154
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
1155
|
+
};
|
|
1156
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
1157
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
1158
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
1159
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
1160
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
1161
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
1162
|
+
};
|
|
1163
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
1164
|
+
var RESERVED_RESOLVED_COLUMN_NAMES = /* @__PURE__ */ new Set([
|
|
1165
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
1166
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
1167
|
+
]);
|
|
1168
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
1169
|
+
var canonicalFunctionRefSchema = z.object({
|
|
1170
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
1171
|
+
kind: z.enum(["managed", "native"]),
|
|
1172
|
+
id: z.string().min(1).max(200)
|
|
1173
|
+
}).strict();
|
|
1174
|
+
var canonicalFetchShapeSchema = z.union([
|
|
1175
|
+
z.object({ shape: z.literal("single") }).strict(),
|
|
1176
|
+
z.object({ shape: z.literal("batch"), maxKeys: z.number().int().min(2).max(500) }).strict()
|
|
1177
|
+
]);
|
|
1178
|
+
var canonicalResolvedSourceSchema = z.object({
|
|
1179
|
+
name: identifier,
|
|
1180
|
+
kind: z.enum(["manual", "etl", "prefetch"]),
|
|
1181
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
1182
|
+
table: identifier,
|
|
1183
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
1184
|
+
columns: z.array(z.string().min(1).max(63)).min(1).max(500),
|
|
1185
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
1186
|
+
updatedAt: z.string().min(1).max(63).optional(),
|
|
1187
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
1188
|
+
key: z.string().min(1).max(63).optional(),
|
|
1189
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
1190
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
1191
|
+
prefetchDefaults: z.object({
|
|
1192
|
+
maxStalenessMs: durationMs,
|
|
1193
|
+
timeoutMs: durationMs,
|
|
1194
|
+
onError: z.enum(["fallback", "fail"])
|
|
1195
|
+
}).strict().optional(),
|
|
1196
|
+
writeback: z.object({
|
|
1197
|
+
columns: z.array(z.string().min(1).max(63)).min(1).max(500),
|
|
1198
|
+
defaultMode: z.enum(["sync", "async"]),
|
|
1199
|
+
timeoutMs: durationMs,
|
|
1200
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
1201
|
+
clearManualOnConfirm: z.boolean()
|
|
1202
|
+
}).strict().optional()
|
|
1203
|
+
}).strict();
|
|
1204
|
+
var canonicalResolveRuleSchema = z.object({
|
|
1205
|
+
column: z.string().min(1).max(63),
|
|
1206
|
+
comparator: z.enum(COMPARATORS),
|
|
1207
|
+
/** Source names, in tie-break order. */
|
|
1208
|
+
candidates: z.array(identifier).min(1).max(16),
|
|
1209
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
1210
|
+
orderBy: z.string().min(1).max(2e3).optional()
|
|
1211
|
+
}).strict();
|
|
1212
|
+
var canonicalResolvedSchema = z.object({
|
|
1213
|
+
sources: z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
1214
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
1215
|
+
resolve: z.array(canonicalResolveRuleSchema).max(500),
|
|
1216
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
1217
|
+
deletedBy: z.array(identifier).max(1).default([]),
|
|
1218
|
+
/**
|
|
1219
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
1220
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
1221
|
+
* resolution query. Omitted when the view computes on read.
|
|
1222
|
+
*/
|
|
1223
|
+
materialized: z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
1224
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
1225
|
+
policies: z.object({
|
|
1226
|
+
write: policyExpressionSchema.optional(),
|
|
1227
|
+
insert: policyExpressionSchema.optional(),
|
|
1228
|
+
update: policyExpressionSchema.optional()
|
|
1229
|
+
}).strict().default({})
|
|
1230
|
+
}).strict();
|
|
1231
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
1232
|
+
const resolved = view.resolved;
|
|
1233
|
+
if (!resolved) return [];
|
|
1234
|
+
const errors = [];
|
|
1235
|
+
const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1236
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
1237
|
+
if (!pkColumn) {
|
|
1238
|
+
fail("requires exactly one primary-key column");
|
|
1239
|
+
return errors;
|
|
1240
|
+
}
|
|
1241
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
1242
|
+
const scope = {
|
|
1243
|
+
view,
|
|
1244
|
+
resolved,
|
|
1245
|
+
pkColumn,
|
|
1246
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1247
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
1248
|
+
fail
|
|
1249
|
+
};
|
|
1250
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
|
|
1251
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
1252
|
+
for (const source of resolved.sources) {
|
|
1253
|
+
if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
|
|
1254
|
+
scope.sourceByName.set(source.name, source);
|
|
1255
|
+
const table = tableByName.get(source.table);
|
|
1256
|
+
if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1257
|
+
else validateSource({ scope, source, table });
|
|
1258
|
+
}
|
|
1259
|
+
validateRules(scope);
|
|
1260
|
+
validateViewCells(scope);
|
|
1261
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
1262
|
+
for (const name of resolved.deletedBy) {
|
|
1263
|
+
if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
|
|
1264
|
+
}
|
|
1265
|
+
return errors;
|
|
1266
|
+
}
|
|
1267
|
+
function validateSource({ scope, source, table }) {
|
|
1268
|
+
const { fail } = scope;
|
|
1269
|
+
if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
|
|
1270
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
1271
|
+
fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
1272
|
+
}
|
|
1273
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
1274
|
+
if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
1275
|
+
if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
1276
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
1277
|
+
validateSourceColumns({ scope, source, byName });
|
|
1278
|
+
if (source.kind === "etl") {
|
|
1279
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
1280
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
1281
|
+
fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
validatePrefetchFields({ scope, source });
|
|
1285
|
+
}
|
|
1286
|
+
function validatePrefetchFields({ scope, source }) {
|
|
1287
|
+
const { fail, pkColumn } = scope;
|
|
1288
|
+
if (source.kind !== "prefetch") {
|
|
1289
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1290
|
+
fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
1291
|
+
}
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1295
|
+
fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1296
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
1297
|
+
fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1298
|
+
}
|
|
1299
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
1300
|
+
if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
1304
|
+
const { fail, pkColumn, dataColumns } = scope;
|
|
1305
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
1306
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
1307
|
+
const theirs = byName.get(column);
|
|
1308
|
+
if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
|
|
1309
|
+
else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
1310
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
|
|
1314
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
1315
|
+
for (const rule of resolved.resolve) {
|
|
1316
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
1317
|
+
fail(`resolve rule targets unknown column '${rule.column}'`);
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
|
|
1321
|
+
ruled.add(rule.column);
|
|
1322
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
1323
|
+
fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
1324
|
+
}
|
|
1325
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
|
|
1326
|
+
for (const candidate of rule.candidates) {
|
|
1327
|
+
const source = sourceByName.get(candidate);
|
|
1328
|
+
if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
1329
|
+
else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
for (const column of dataColumns.keys()) {
|
|
1333
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
var VIEW_CELL_TYPES = {
|
|
1337
|
+
source: "text",
|
|
1338
|
+
at: "timestamptz",
|
|
1339
|
+
by: "text",
|
|
1340
|
+
conflicts: "jsonb"
|
|
1341
|
+
};
|
|
1342
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail }) {
|
|
1343
|
+
const expected = /* @__PURE__ */ new Map();
|
|
1344
|
+
for (const column of dataColumns.values()) {
|
|
1345
|
+
if (column.name === pkColumn.name) continue;
|
|
1346
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
1347
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
for (const column of view.columns) {
|
|
1351
|
+
if (!column.cell) continue;
|
|
1352
|
+
const want = expected.get(column.name);
|
|
1353
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
1354
|
+
fail(`unexpected cell column '${column.name}'`);
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
1358
|
+
expected.delete(column.name);
|
|
1359
|
+
}
|
|
1360
|
+
for (const name of expected.keys()) fail(`missing cell column '${name}'`);
|
|
1361
|
+
}
|
|
1362
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
1363
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
1364
|
+
const { view, resolved, fail } = scope;
|
|
1365
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
1366
|
+
const table = tableByName.get(tableName);
|
|
1367
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
1368
|
+
fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
1369
|
+
} else {
|
|
1370
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
1371
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
|
|
1372
|
+
}
|
|
1373
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
1374
|
+
if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
|
|
1375
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
|
|
1376
|
+
for (const source of resolved.sources) {
|
|
1377
|
+
if (source.kind !== "etl") continue;
|
|
1378
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
1379
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
1380
|
+
if (!unconditional) {
|
|
1381
|
+
fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/canonical/v2.ts
|
|
879
1387
|
var CANONICAL_FORMAT = 2;
|
|
880
1388
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
881
|
-
var
|
|
1389
|
+
var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
882
1390
|
var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
883
1391
|
var COLUMN_TYPES = [
|
|
884
1392
|
"uuid",
|
|
@@ -894,14 +1402,14 @@ var COLUMN_TYPES = [
|
|
|
894
1402
|
];
|
|
895
1403
|
var columnTypeSchema = z.enum(COLUMN_TYPES);
|
|
896
1404
|
var canonicalEnumSchema = z.object({
|
|
897
|
-
name:
|
|
1405
|
+
name: identifier2,
|
|
898
1406
|
values: z.array(z.string().min(1).max(63)).min(1).max(100)
|
|
899
1407
|
});
|
|
900
1408
|
var canonicalColumnSchema = z.object({
|
|
901
1409
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
902
1410
|
name: z.string().min(1).max(63),
|
|
903
1411
|
columnName: columnIdentifier,
|
|
904
|
-
type: z.union([columnTypeSchema, z.object({ enum:
|
|
1412
|
+
type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
|
|
905
1413
|
nullable: z.boolean().default(true),
|
|
906
1414
|
unique: z.boolean().default(false),
|
|
907
1415
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -917,7 +1425,14 @@ var canonicalColumnSchema = z.object({
|
|
|
917
1425
|
/** Migration hints — same semantics as v1. */
|
|
918
1426
|
renamedFrom: columnIdentifier.optional(),
|
|
919
1427
|
cast: z.string().max(200).optional(),
|
|
920
|
-
deprecated: z.boolean().default(false)
|
|
1428
|
+
deprecated: z.boolean().default(false),
|
|
1429
|
+
/**
|
|
1430
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
1431
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
1432
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
1433
|
+
* ordinary columns so their hashes are unchanged.
|
|
1434
|
+
*/
|
|
1435
|
+
cell: z.object({ of: z.string().min(1).max(63), part: z.enum(CELL_PARTS) }).strict().optional()
|
|
921
1436
|
}).superRefine((column, ctx) => {
|
|
922
1437
|
if (column.onUpdateFn === void 0) return;
|
|
923
1438
|
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
@@ -932,36 +1447,26 @@ var canonicalLinkSchema = z.object({
|
|
|
932
1447
|
name: z.string().min(1).max(63),
|
|
933
1448
|
kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
934
1449
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
935
|
-
target:
|
|
1450
|
+
target: identifier2.optional(),
|
|
936
1451
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
937
1452
|
on: columnIdentifier,
|
|
938
1453
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
939
1454
|
references: columnIdentifier.optional(),
|
|
940
1455
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
941
1456
|
enforce: z.boolean().default(false),
|
|
1457
|
+
onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
1458
|
+
onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
942
1459
|
description: z.string().max(500).optional()
|
|
943
1460
|
});
|
|
944
1461
|
var canonicalIndexSchema = z.object({
|
|
945
1462
|
columns: z.array(columnIdentifier).min(1),
|
|
946
1463
|
unique: z.boolean().default(false),
|
|
947
|
-
name:
|
|
1464
|
+
name: identifier2.optional()
|
|
948
1465
|
});
|
|
949
|
-
var
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
allow: z.literal(true).optional(),
|
|
954
|
-
policy: z.string().min(1).max(2e3).optional()
|
|
955
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
956
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
957
|
-
});
|
|
958
|
-
var policyExpressionSchema = z.union([
|
|
959
|
-
z.string().min(1).max(2e3),
|
|
960
|
-
z.object({
|
|
961
|
-
default: z.string().min(1).max(2e3),
|
|
962
|
-
variants: z.array(policyVariantSchema).max(10).default([])
|
|
963
|
-
})
|
|
964
|
-
]);
|
|
1466
|
+
var canonicalCheckSchema = z.object({
|
|
1467
|
+
name: identifier2,
|
|
1468
|
+
expression: z.string().min(1).max(2e3)
|
|
1469
|
+
}).strict();
|
|
965
1470
|
var canonicalPoliciesSchema = z.object({
|
|
966
1471
|
read: policyExpressionSchema.optional(),
|
|
967
1472
|
write: policyExpressionSchema.optional(),
|
|
@@ -1149,7 +1654,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
|
1149
1654
|
"width_bucket"
|
|
1150
1655
|
];
|
|
1151
1656
|
var canonicalActionSchema = z.object({
|
|
1152
|
-
name:
|
|
1657
|
+
name: identifier2,
|
|
1153
1658
|
requires: z.string().max(1e3).optional(),
|
|
1154
1659
|
params: z.record(z.string(), z.object({
|
|
1155
1660
|
type: columnTypeSchema,
|
|
@@ -1161,9 +1666,9 @@ var canonicalActionSchema = z.object({
|
|
|
1161
1666
|
});
|
|
1162
1667
|
var canonicalCowMutationSchema = z.object({
|
|
1163
1668
|
kind: z.enum(["override", "revert"]),
|
|
1164
|
-
name:
|
|
1669
|
+
name: identifier2,
|
|
1165
1670
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
1166
|
-
synced:
|
|
1671
|
+
synced: identifier2,
|
|
1167
1672
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
1168
1673
|
pk: z.string().min(1).max(63),
|
|
1169
1674
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -1191,23 +1696,27 @@ var canonicalCowMutationSchema = z.object({
|
|
|
1191
1696
|
}
|
|
1192
1697
|
});
|
|
1193
1698
|
var canonicalForeignKeySchema = z.object({
|
|
1194
|
-
name:
|
|
1699
|
+
name: identifier2,
|
|
1195
1700
|
columns: z.array(columnIdentifier).min(1).max(8),
|
|
1196
|
-
target:
|
|
1701
|
+
target: identifier2,
|
|
1197
1702
|
references: z.array(columnIdentifier).min(1).max(8),
|
|
1198
|
-
deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
1703
|
+
deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
1704
|
+
onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
1705
|
+
onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
1199
1706
|
}).strict();
|
|
1200
1707
|
var canonicalTableSchema = z.object({
|
|
1201
|
-
table:
|
|
1708
|
+
table: identifier2,
|
|
1202
1709
|
description: z.string().max(2e3).optional(),
|
|
1203
1710
|
/** Table-level rename hint (new in v2). */
|
|
1204
|
-
renamedFrom:
|
|
1711
|
+
renamedFrom: identifier2.optional(),
|
|
1205
1712
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
1206
1713
|
source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
1207
1714
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
1208
1715
|
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1209
1716
|
links: z.array(canonicalLinkSchema).default([]),
|
|
1210
1717
|
indexes: z.array(canonicalIndexSchema).default([]),
|
|
1718
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1719
|
+
checks: z.array(canonicalCheckSchema).max(100).optional(),
|
|
1211
1720
|
policies: canonicalPoliciesSchema.default({}),
|
|
1212
1721
|
/**
|
|
1213
1722
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -1218,22 +1727,55 @@ var canonicalTableSchema = z.object({
|
|
|
1218
1727
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
1219
1728
|
cowMutations: z.array(canonicalCowMutationSchema).optional(),
|
|
1220
1729
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1221
|
-
foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional()
|
|
1730
|
+
foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
1731
|
+
/**
|
|
1732
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
1733
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
1734
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
1735
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
1736
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
1737
|
+
*/
|
|
1738
|
+
cellMetadata: z.literal(true).optional(),
|
|
1739
|
+
/**
|
|
1740
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
1741
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
1742
|
+
* only the data plane writes them. Omitted when false.
|
|
1743
|
+
*/
|
|
1744
|
+
internal: z.literal(true).optional(),
|
|
1745
|
+
/**
|
|
1746
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
1747
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
1748
|
+
* data plane. Omitted otherwise.
|
|
1749
|
+
*/
|
|
1750
|
+
materializes: identifier2.optional()
|
|
1222
1751
|
});
|
|
1752
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
1753
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
1223
1754
|
var canonicalViewSchema = z.object({
|
|
1224
|
-
view:
|
|
1755
|
+
view: identifier2,
|
|
1225
1756
|
description: z.string().max(2e3).optional(),
|
|
1226
1757
|
columns: z.array(canonicalColumnSchema).min(1),
|
|
1227
1758
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
1228
1759
|
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1229
1760
|
links: z.array(canonicalLinkSchema).default([]),
|
|
1230
1761
|
policies: z.object({ read: policyExpressionSchema }).strict(),
|
|
1231
|
-
query: z.string().min(1).max(
|
|
1762
|
+
query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
1232
1763
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
1233
1764
|
}),
|
|
1234
1765
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
1235
|
-
dependencies: z.array(
|
|
1766
|
+
dependencies: z.array(identifier2).max(500),
|
|
1767
|
+
/**
|
|
1768
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
1769
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
1770
|
+
* existing views hash unchanged.
|
|
1771
|
+
*/
|
|
1772
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
1773
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
1774
|
+
internal: z.literal(true).optional()
|
|
1236
1775
|
}).superRefine((view, ctx) => {
|
|
1776
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
1777
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
1778
|
+
}
|
|
1237
1779
|
for (const [index3, column] of view.columns.entries()) {
|
|
1238
1780
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1239
1781
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
@@ -1248,15 +1790,82 @@ var canonicalViewSchema = z.object({
|
|
|
1248
1790
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
|
|
1249
1791
|
}
|
|
1250
1792
|
}
|
|
1793
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
1794
|
+
for (const [index3, column] of view.columns.entries()) {
|
|
1795
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index3], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1251
1798
|
for (const [index3, link] of view.links.entries()) {
|
|
1252
1799
|
if (link.enforce) {
|
|
1253
1800
|
ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
1254
1801
|
}
|
|
1802
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
1803
|
+
ctx.addIssue({ code: "custom", path: ["links", index3], message: `view link '${link.name}' cannot declare referential actions` });
|
|
1804
|
+
}
|
|
1255
1805
|
}
|
|
1256
1806
|
});
|
|
1807
|
+
var CELL_PART_TYPES = {
|
|
1808
|
+
at: "timestamptz",
|
|
1809
|
+
by: "text",
|
|
1810
|
+
rev: "uuid",
|
|
1811
|
+
source: "text",
|
|
1812
|
+
conflicts: "jsonb"
|
|
1813
|
+
};
|
|
1814
|
+
function trackedCellColumns(table) {
|
|
1815
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
1816
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
1817
|
+
}
|
|
1818
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1819
|
+
function validateCellMetadata(table, index3, ctx) {
|
|
1820
|
+
const fail = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
|
|
1821
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
1822
|
+
if (table.materializes !== void 0) return;
|
|
1823
|
+
if (table.cellMetadata !== true) {
|
|
1824
|
+
for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index3, "columns", position]);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (table.source !== void 0) {
|
|
1828
|
+
fail(`synced table '${table.table}' cannot track cell metadata`);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
const tracked = trackedCellColumns(table);
|
|
1832
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
1833
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
1834
|
+
if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
1835
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1836
|
+
for (const { column, position, cell } of cells) {
|
|
1837
|
+
const path = ["tables", index3, "columns", position];
|
|
1838
|
+
const owner = byName.get(cell.of);
|
|
1839
|
+
if (!owner) {
|
|
1840
|
+
fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
1844
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
1845
|
+
fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
1846
|
+
}
|
|
1847
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1848
|
+
fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
1849
|
+
}
|
|
1850
|
+
const key = `${cell.of}:${cell.part}`;
|
|
1851
|
+
if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
|
|
1852
|
+
seen.add(key);
|
|
1853
|
+
}
|
|
1854
|
+
for (const column of tracked) {
|
|
1855
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
1856
|
+
fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
1857
|
+
}
|
|
1858
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
1859
|
+
fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
1860
|
+
}
|
|
1861
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
1862
|
+
if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1257
1866
|
var canonicalDefinitionSchema = z.object({
|
|
1258
1867
|
definition_format: z.literal(CANONICAL_FORMAT),
|
|
1259
|
-
namespace:
|
|
1868
|
+
namespace: identifier2.max(41),
|
|
1260
1869
|
description: z.string().max(2e3).optional(),
|
|
1261
1870
|
enums: z.array(canonicalEnumSchema).default([]),
|
|
1262
1871
|
tables: z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -1274,6 +1883,7 @@ var canonicalDefinitionSchema = z.object({
|
|
|
1274
1883
|
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
1275
1884
|
}
|
|
1276
1885
|
seen.add(table.table);
|
|
1886
|
+
validateCellMetadata(table, index3, ctx);
|
|
1277
1887
|
if (table.source !== void 0) {
|
|
1278
1888
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1279
1889
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -1293,6 +1903,26 @@ var canonicalDefinitionSchema = z.object({
|
|
|
1293
1903
|
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
1294
1904
|
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
1295
1905
|
]);
|
|
1906
|
+
for (const [index3, link] of table.links.entries()) {
|
|
1907
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
1908
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "referential actions require an enforced toOne link" });
|
|
1909
|
+
}
|
|
1910
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
1911
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
1912
|
+
if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "SET NULL requires a nullable source column" });
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
for (const [index3, check] of (table.checks ?? []).entries()) {
|
|
1916
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index3], message });
|
|
1917
|
+
if (table.source) fail("synced tables cannot declare checks");
|
|
1918
|
+
if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
|
|
1919
|
+
names.add(check.name);
|
|
1920
|
+
try {
|
|
1921
|
+
compileCheckExpression(check.expression, table.columns);
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1296
1926
|
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
1297
1927
|
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
|
|
1298
1928
|
if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
|
|
@@ -1310,10 +1940,18 @@ var canonicalDefinitionSchema = z.object({
|
|
|
1310
1940
|
if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
|
|
1311
1941
|
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
|
|
1312
1942
|
}
|
|
1943
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
1944
|
+
if (fk.columns.some((name) => table.columns.find((column) => column.columnName === name)?.nullable === false || table.primaryKey?.includes(name))) fail("SET NULL requires nullable source columns");
|
|
1945
|
+
}
|
|
1313
1946
|
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
1314
1947
|
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
|
|
1315
1948
|
}
|
|
1316
1949
|
}
|
|
1950
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
1951
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
1952
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1317
1955
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1318
1956
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1319
1957
|
const cowMutations = table.cowMutations ?? [];
|
|
@@ -1437,8 +2075,12 @@ function canonicalRelations(definition) {
|
|
|
1437
2075
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
1438
2076
|
links: view.links,
|
|
1439
2077
|
indexes: [],
|
|
1440
|
-
|
|
1441
|
-
|
|
2078
|
+
// A resolved table's write policies (evaluated against the resolved row)
|
|
2079
|
+
// ride alongside the view's read policy.
|
|
2080
|
+
policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
|
|
2081
|
+
actions: [],
|
|
2082
|
+
...view.resolved !== void 0 ? { resolved: view.resolved } : {},
|
|
2083
|
+
...view.internal ? { internal: true } : {}
|
|
1442
2084
|
}));
|
|
1443
2085
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
1444
2086
|
}
|
|
@@ -1448,13 +2090,14 @@ function normalizeCanonical(definition) {
|
|
|
1448
2090
|
...definition,
|
|
1449
2091
|
enums: sortByName(definition.enums),
|
|
1450
2092
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
1451
|
-
const { cowMutations, foreignKeys, ...rest } = t2;
|
|
2093
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t2;
|
|
1452
2094
|
return {
|
|
1453
2095
|
...rest,
|
|
1454
2096
|
links: sortByName(t2.links),
|
|
1455
2097
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
1456
2098
|
actions: sortByName(t2.actions),
|
|
1457
2099
|
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
2100
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
1458
2101
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
1459
2102
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
1460
2103
|
};
|
|
@@ -1469,6 +2112,454 @@ function normalizeCanonical(definition) {
|
|
|
1469
2112
|
}
|
|
1470
2113
|
return normalized;
|
|
1471
2114
|
}
|
|
2115
|
+
|
|
2116
|
+
// src/authoring/resolved-sources.ts
|
|
2117
|
+
function cloneColumn({ builder, property, notNull, primaryKey: primaryKey2 }) {
|
|
2118
|
+
const canonical = builder.toCanonicalColumn(property);
|
|
2119
|
+
let clone = new ColumnBuilder(canonical.type, canonical.columnName);
|
|
2120
|
+
if (primaryKey2) clone = clone.primaryKey();
|
|
2121
|
+
if (notNull) clone = clone.notNull();
|
|
2122
|
+
if (canonical.description !== void 0) clone = clone.description(canonical.description);
|
|
2123
|
+
return clone;
|
|
2124
|
+
}
|
|
2125
|
+
function planSource(input) {
|
|
2126
|
+
const { sourceName, input: source, fail } = input;
|
|
2127
|
+
if (!IDENTIFIER_PATTERN.test(sourceName)) fail(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
|
|
2128
|
+
switch (source.kind) {
|
|
2129
|
+
case "manual":
|
|
2130
|
+
return {
|
|
2131
|
+
name: sourceName,
|
|
2132
|
+
kind: "manual",
|
|
2133
|
+
columns: input.dataColumns,
|
|
2134
|
+
table: internalTable(input, input.dataColumns, {
|
|
2135
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned(),
|
|
2136
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName),
|
|
2137
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName),
|
|
2138
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.columnName)
|
|
2139
|
+
}, { revisions: true })
|
|
2140
|
+
};
|
|
2141
|
+
case "etl":
|
|
2142
|
+
return planEtl(input, source);
|
|
2143
|
+
case "prefetch":
|
|
2144
|
+
return planPrefetch(input, source);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
|
|
2148
|
+
function internalTable(input, supplied, system, options = {}) {
|
|
2149
|
+
const { viewName, sourceName, columns, pk, pkBuilder } = input;
|
|
2150
|
+
const tableColumns = { [pk]: cloneColumn({ builder: pkBuilder, property: pk, notNull: true, primaryKey: true }) };
|
|
2151
|
+
for (const property of supplied) {
|
|
2152
|
+
tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
|
|
2153
|
+
}
|
|
2154
|
+
return defineTable(`${viewName}__${sourceName}`, { ...tableColumns, ...system }, {
|
|
2155
|
+
description: `Internal ${input.input.kind} source "${sourceName}" of resolved table "${viewName}". System-maintained.`,
|
|
2156
|
+
cellMetadata: true,
|
|
2157
|
+
// The manual source identifies each edit (`c__rev`) so an async
|
|
2158
|
+
// confirmation clears only the exact edit it delivered.
|
|
2159
|
+
...options.revisions ? { cellRevisions: true } : {},
|
|
2160
|
+
internal: true
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
function assertColumns(input, list, what) {
|
|
2164
|
+
for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2165
|
+
}
|
|
2166
|
+
function planEtl(input, source) {
|
|
2167
|
+
const { sourceName, pk, dataColumns, fail } = input;
|
|
2168
|
+
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2169
|
+
if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2170
|
+
const synced = table;
|
|
2171
|
+
if (!(pk in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
|
|
2172
|
+
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2173
|
+
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2174
|
+
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
2175
|
+
fail(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
|
|
2176
|
+
}
|
|
2177
|
+
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2178
|
+
}
|
|
2179
|
+
function planPrefetch(input, source) {
|
|
2180
|
+
const { sourceName, pk, fail } = input;
|
|
2181
|
+
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2182
|
+
if (source.key !== pk && !source.columns.includes(source.key)) {
|
|
2183
|
+
fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2184
|
+
}
|
|
2185
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
2186
|
+
if (!source.columns.includes(column)) fail(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
|
|
2187
|
+
}
|
|
2188
|
+
const writeback = source.writeback;
|
|
2189
|
+
return {
|
|
2190
|
+
name: sourceName,
|
|
2191
|
+
kind: "prefetch",
|
|
2192
|
+
columns: [...source.columns],
|
|
2193
|
+
key: source.key,
|
|
2194
|
+
function: source.function,
|
|
2195
|
+
fetch: source.fetch === "single" ? { shape: "single" } : { shape: "batch", maxKeys: source.fetch.batch.maxKeys },
|
|
2196
|
+
prefetchDefaults: {
|
|
2197
|
+
maxStalenessMs: parseDuration(source.prefetchDefaults?.maxStaleness ?? "30s"),
|
|
2198
|
+
timeoutMs: parseDuration(source.prefetchDefaults?.timeout ?? "1500ms"),
|
|
2199
|
+
onError: source.prefetchDefaults?.onError ?? "fallback"
|
|
2200
|
+
},
|
|
2201
|
+
...writeback ? {
|
|
2202
|
+
writeback: {
|
|
2203
|
+
columns: [...writeback.columns],
|
|
2204
|
+
defaultMode: writeback.defaultMode ?? "sync",
|
|
2205
|
+
timeoutMs: parseDuration(writeback.timeout ?? "5s"),
|
|
2206
|
+
clearManualOnConfirm: writeback.clearManualOnConfirm ?? true
|
|
2207
|
+
}
|
|
2208
|
+
} : {},
|
|
2209
|
+
table: internalTable(input, source.columns, {
|
|
2210
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.columnName).notNull().default("now()"),
|
|
2211
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned()
|
|
2212
|
+
})
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
function parseDuration(input) {
|
|
2216
|
+
const value = parseDurationMs(input);
|
|
2217
|
+
if (value === void 0) throw new OrmDefinitionError(`invalid duration "${input}" \u2014 use e.g. '1500ms', '30s', '5m'`);
|
|
2218
|
+
return value;
|
|
2219
|
+
}
|
|
2220
|
+
var PG_TYPE = {
|
|
2221
|
+
uuid: "uuid",
|
|
2222
|
+
text: "text",
|
|
2223
|
+
integer: "integer",
|
|
2224
|
+
bigint: "bigint",
|
|
2225
|
+
numeric: "numeric",
|
|
2226
|
+
double: "double precision",
|
|
2227
|
+
boolean: "boolean",
|
|
2228
|
+
timestamptz: "timestamptz",
|
|
2229
|
+
date: "date",
|
|
2230
|
+
jsonb: "jsonb"
|
|
2231
|
+
};
|
|
2232
|
+
var ident = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
2233
|
+
var literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
2234
|
+
var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
|
|
2235
|
+
function candidateSql({ source, column, pk, pgType }) {
|
|
2236
|
+
const a = ident(`s_${source.name}`);
|
|
2237
|
+
const name = physical({ source, property: column });
|
|
2238
|
+
const value = `CAST(${a}.${ident(name)} AS ${pgType})`;
|
|
2239
|
+
if (source.kind === "etl") {
|
|
2240
|
+
return {
|
|
2241
|
+
source: source.name,
|
|
2242
|
+
value,
|
|
2243
|
+
has: `(${a}.${ident(physical({ source, property: pk }))} IS NOT NULL)`,
|
|
2244
|
+
at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
|
|
2245
|
+
by: "CAST(NULL AS text)"
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
const at = `${a}.${ident(cellColumnName(name, "at"))}`;
|
|
2249
|
+
const tombstoned2 = source.kind === "prefetch" ? ` AND NOT ${a}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName)}` : "";
|
|
2250
|
+
return { source: source.name, value, has: `(${at} IS NOT NULL${tombstoned2})`, at, by: `${a}.${ident(cellColumnName(name, "by"))}` };
|
|
2251
|
+
}
|
|
2252
|
+
function outranks({ i, j, key, direction, inclusive }) {
|
|
2253
|
+
const ki = i[key];
|
|
2254
|
+
const kj = j[key];
|
|
2255
|
+
const op = inclusive ? `${direction}=` : direction;
|
|
2256
|
+
return inclusive ? `(${kj} IS NULL OR (${ki} IS NOT NULL AND ${ki} ${op} ${kj}))` : `(${ki} IS NOT NULL AND (${kj} IS NULL OR ${ki} ${op} ${kj}))`;
|
|
2257
|
+
}
|
|
2258
|
+
function rankedWinner({ candidates, key, direction }) {
|
|
2259
|
+
const branches = candidates.map((candidate, index3) => {
|
|
2260
|
+
const beats = candidates.map((other, otherIndex) => otherIndex === index3 ? null : `(NOT ${other.has} OR ${outranks({ i: candidate, j: other, key, direction, inclusive: index3 < otherIndex })})`).filter((clause) => clause !== null);
|
|
2261
|
+
return `WHEN ${[candidate.has, ...beats].join(" AND ")} THEN ${index3 + 1}`;
|
|
2262
|
+
});
|
|
2263
|
+
return `CASE ${branches.join(" ")} END`;
|
|
2264
|
+
}
|
|
2265
|
+
function winnerIndex({ comparator, candidates }) {
|
|
2266
|
+
const firstWith = (test) => candidates.map((candidate, index3) => `WHEN ${test(candidate)} THEN ${index3 + 1}`).join(" ");
|
|
2267
|
+
if (candidates.length === 1 || comparator === "priority") return `CASE ${firstWith((c) => c.has)} END`;
|
|
2268
|
+
switch (comparator) {
|
|
2269
|
+
case "latest":
|
|
2270
|
+
return rankedWinner({ candidates, key: "at", direction: ">" });
|
|
2271
|
+
case "highest":
|
|
2272
|
+
return rankedWinner({ candidates, key: "value", direction: ">" });
|
|
2273
|
+
case "lowest":
|
|
2274
|
+
return rankedWinner({ candidates, key: "value", direction: "<" });
|
|
2275
|
+
case "nonNullFirst":
|
|
2276
|
+
return `CASE ${firstWith((c) => `${c.has} AND ${c.value} IS NOT NULL`)} ${firstWith((c) => c.has)} END`;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
function pick(winner, candidates, field) {
|
|
2280
|
+
return `CASE ${winner} ${candidates.map((candidate, index3) => `WHEN ${index3 + 1} THEN ${field(candidate)}`).join(" ")} END`;
|
|
2281
|
+
}
|
|
2282
|
+
function loserSql({ loser, winner }) {
|
|
2283
|
+
return `CASE WHEN ${loser.has} AND ${loser.value} IS DISTINCT FROM ${winner.value} AND ${loser.at} > ${winner.at} THEN jsonb_build_array(jsonb_build_object('source', ${literal(loser.source)}, 'value', to_jsonb(${loser.value}), 'at', ${loser.at})) ELSE '[]'::jsonb END`;
|
|
2284
|
+
}
|
|
2285
|
+
function losersAgainst({ candidates, winner, isWinner }) {
|
|
2286
|
+
const losers = candidates.flatMap((loser, index3) => {
|
|
2287
|
+
const excluded = isWinner(loser, index3);
|
|
2288
|
+
if (excluded === true) return [];
|
|
2289
|
+
const clause = loserSql({ loser, winner });
|
|
2290
|
+
return [excluded === false ? clause : `CASE WHEN ${excluded} THEN '[]'::jsonb ELSE ${clause} END`];
|
|
2291
|
+
});
|
|
2292
|
+
return losers.length === 0 ? "CAST(NULL AS jsonb)" : `NULLIF(${losers.join(" || ")}, '[]'::jsonb)`;
|
|
2293
|
+
}
|
|
2294
|
+
function builtinCell({ comparator, candidates }) {
|
|
2295
|
+
const winner = winnerIndex({ comparator, candidates });
|
|
2296
|
+
const conflicts = comparator === "latest" || candidates.length < 2 ? "CAST(NULL AS jsonb)" : `CASE ${winner} ${candidates.map((known, index3) => `WHEN ${index3 + 1} THEN ${losersAgainst({ candidates, winner: known, isWinner: (_c, other) => other === index3 })}`).join(" ")} END`;
|
|
2297
|
+
return {
|
|
2298
|
+
value: pick(winner, candidates, (c) => c.value),
|
|
2299
|
+
parts: {
|
|
2300
|
+
source: pick(winner, candidates, (c) => literal(c.source)),
|
|
2301
|
+
at: pick(winner, candidates, (c) => c.at),
|
|
2302
|
+
by: pick(winner, candidates, (c) => c.by),
|
|
2303
|
+
conflicts
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
function customCell({ candidates, orderBy, index: index3 }) {
|
|
2308
|
+
const w = ident(`w_${index3}`);
|
|
2309
|
+
const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
|
|
2310
|
+
const lateral = ` LEFT JOIN LATERAL (SELECT max(winner.source) AS source, (array_agg(winner.value))[1] AS value, max(winner.updated_at) AS updated_at, max(winner.actor) AS actor FROM (WITH cand(source, has, value, updated_at, actor, ord) AS (VALUES ${rows.join(", ")}) SELECT c.source, c.value, c.updated_at, c.actor FROM cand c WHERE c.has ORDER BY ${orderBy}, c.ord LIMIT 1) AS winner) AS ${w} ON true`;
|
|
2311
|
+
const winner = { value: `${w}.value`, at: `${w}.updated_at` };
|
|
2312
|
+
return {
|
|
2313
|
+
value: winner.value,
|
|
2314
|
+
lateral,
|
|
2315
|
+
parts: {
|
|
2316
|
+
source: `${w}.source`,
|
|
2317
|
+
at: winner.at,
|
|
2318
|
+
by: `${w}.actor`,
|
|
2319
|
+
conflicts: candidates.length < 2 ? "CAST(NULL AS jsonb)" : losersAgainst({ candidates, winner, isWinner: (c) => `${w}.source = ${literal(c.source)}` })
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
function buildResolvedQuery({ context, plan }) {
|
|
2324
|
+
const { qb, relation } = context;
|
|
2325
|
+
const pkColumn = plan.columns[plan.pk].columnNameFor(plan.pk);
|
|
2326
|
+
const alias = (source) => ident(`s_${source}`);
|
|
2327
|
+
const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
|
|
2328
|
+
const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
|
|
2329
|
+
const schema = getTableConfig(relation(plan.plans[0].table)).schema;
|
|
2330
|
+
const keyBranches = plan.plans.map((source) => {
|
|
2331
|
+
const where = source.kind === "prefetch" ? sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : sql.raw("");
|
|
2332
|
+
const sourcePk = ident(physical({ source, property: plan.pk }));
|
|
2333
|
+
const selectedPk = sourcePk === ident(pkColumn) ? sourcePk : `${sourcePk} AS ${ident(pkColumn)}`;
|
|
2334
|
+
return sql`SELECT ${sql.raw(selectedPk)} FROM ${relation(source.table)}${where}`;
|
|
2335
|
+
});
|
|
2336
|
+
const joins = plan.plans.map(
|
|
2337
|
+
(source) => sql` LEFT JOIN ${relation(source.table)} AS ${sql.raw(alias(source.name))} ON ${sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
|
|
2338
|
+
);
|
|
2339
|
+
const cells = /* @__PURE__ */ new Map();
|
|
2340
|
+
plan.rules.forEach((rule, index3) => {
|
|
2341
|
+
const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
|
|
2342
|
+
const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
|
|
2343
|
+
const candidates = rule.candidates.map((name) => candidateSql({ source: sourceByName.get(name), column: rule.column, pk: plan.pk, pgType }));
|
|
2344
|
+
cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index: index3 }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2345
|
+
});
|
|
2346
|
+
const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [sql.raw(cell.lateral)] : []);
|
|
2347
|
+
const from = sql.join([sql`(${sql.join(keyBranches, sql.raw(" UNION "))}) AS "k"`, ...joins, ...laterals], sql.raw(""));
|
|
2348
|
+
const selected = {
|
|
2349
|
+
[plan.pk]: sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
|
|
2350
|
+
};
|
|
2351
|
+
for (const [property, builder] of Object.entries(plan.columns)) {
|
|
2352
|
+
if (property === plan.pk) continue;
|
|
2353
|
+
const cell = cells.get(property);
|
|
2354
|
+
const columnName = builder.columnNameFor(property);
|
|
2355
|
+
selected[property] = sql.raw(cell.value).as(columnName);
|
|
2356
|
+
for (const [part, expression] of Object.entries(cell.parts)) {
|
|
2357
|
+
selected[cellColumnName(property, part)] = sql.raw(expression).as(cellColumnName(columnName, part));
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const manual = plan.plans.find((source) => source.kind === "manual");
|
|
2361
|
+
const system = RESOLVED_VIEW_SYSTEM_COLUMNS;
|
|
2362
|
+
selected[system.writebackStatus.name] = sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName)}`).as(system.writebackStatus.columnName);
|
|
2363
|
+
selected[system.writebackError.name] = sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName)}`).as(system.writebackError.columnName);
|
|
2364
|
+
const hidden = plan.deletedBy.map((name) => `NOT COALESCE(${alias(name)}.${ident(tombstoned2)}, false)`);
|
|
2365
|
+
const query = qb.select(selected).from(from);
|
|
2366
|
+
return hidden.length > 0 ? query.where(sql.raw(hidden.join(" AND "))) : query;
|
|
2367
|
+
}
|
|
2368
|
+
function managedFn(id) {
|
|
2369
|
+
return { kind: "managed", id };
|
|
2370
|
+
}
|
|
2371
|
+
function manualSource() {
|
|
2372
|
+
return { kind: "manual" };
|
|
2373
|
+
}
|
|
2374
|
+
function etlSource(input) {
|
|
2375
|
+
return { kind: "etl", ...input };
|
|
2376
|
+
}
|
|
2377
|
+
function prefetchSource(input) {
|
|
2378
|
+
return { kind: "prefetch", ...input };
|
|
2379
|
+
}
|
|
2380
|
+
var latest = (candidates) => ({ kind: "latest", candidates });
|
|
2381
|
+
var highest = (candidates) => ({ kind: "highest", candidates });
|
|
2382
|
+
var lowest = (candidates) => ({ kind: "lowest", candidates });
|
|
2383
|
+
var priority = (candidates) => ({ kind: "priority", candidates });
|
|
2384
|
+
var nonNullFirst = (candidates) => ({ kind: "nonNullFirst", candidates });
|
|
2385
|
+
var custom = (candidates, orderBy) => ({ kind: "custom", candidates, orderBy });
|
|
2386
|
+
var SYSTEM_VIEW_COLUMNS = {
|
|
2387
|
+
[RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.columnName),
|
|
2388
|
+
[RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.columnName)
|
|
2389
|
+
};
|
|
2390
|
+
function defineResolvedTable(name, columns, config) {
|
|
2391
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
2392
|
+
const fail = (message) => {
|
|
2393
|
+
throw new OrmDefinitionError(`resolved table "${name}": ${message}`, file);
|
|
2394
|
+
};
|
|
2395
|
+
if (!IDENTIFIER_PATTERN.test(name)) fail(`name must match ${IDENTIFIER_PATTERN}`);
|
|
2396
|
+
for (const property of Object.keys(columns)) {
|
|
2397
|
+
if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
|
|
2398
|
+
fail(`column "${property}" collides with a reserved system column`);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
for (const property of Object.keys(columns)) {
|
|
2402
|
+
if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
|
|
2403
|
+
fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
|
|
2407
|
+
if (pkEntries.length !== 1) fail("declare exactly one .primaryKey() column");
|
|
2408
|
+
const [pk, pkBuilder] = pkEntries[0];
|
|
2409
|
+
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2410
|
+
if (dataColumns.length === 0) fail("declare at least one non-key column");
|
|
2411
|
+
const sourceEntries = Object.entries(config.sources);
|
|
2412
|
+
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
|
|
2413
|
+
const plans = sourceEntries.map(
|
|
2414
|
+
([sourceName, input]) => planSource({ viewName: name, sourceName, input, columns, pk, pkBuilder, dataColumns, fail })
|
|
2415
|
+
);
|
|
2416
|
+
const rules = planRules({ config, plans, dataColumns, fail });
|
|
2417
|
+
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2418
|
+
for (const source of deletedBy) {
|
|
2419
|
+
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
|
|
2420
|
+
}
|
|
2421
|
+
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2422
|
+
const viewColumns = {};
|
|
2423
|
+
for (const [property, builder] of Object.entries(columns)) {
|
|
2424
|
+
viewColumns[property] = cloneColumn({ builder, property, notNull: property === pk, primaryKey: property === pk });
|
|
2425
|
+
if (property === pk) continue;
|
|
2426
|
+
const columnName = builder.columnNameFor(property);
|
|
2427
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
2428
|
+
viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
for (const [property, make] of Object.entries(SYSTEM_VIEW_COLUMNS)) viewColumns[property] = make();
|
|
2432
|
+
const { read, ...writePolicies } = config.policies;
|
|
2433
|
+
const resolved = {
|
|
2434
|
+
internalTables: plans.filter((plan) => plan.kind !== "etl").map((plan) => plan.table),
|
|
2435
|
+
sources: plans,
|
|
2436
|
+
rules,
|
|
2437
|
+
deletedBy,
|
|
2438
|
+
policies: writePolicies
|
|
2439
|
+
};
|
|
2440
|
+
const queryPlan = { columns, pk, plans, rules, deletedBy };
|
|
2441
|
+
const typedColumns = viewColumns;
|
|
2442
|
+
const surface = {
|
|
2443
|
+
...config.description !== void 0 ? { description: config.description } : {},
|
|
2444
|
+
...config.links !== void 0 ? { links: config.links } : {},
|
|
2445
|
+
policies: { read }
|
|
2446
|
+
};
|
|
2447
|
+
if (!config.materialize) {
|
|
2448
|
+
return defineView(name, typedColumns, { ...surface, query: (context) => buildResolvedQuery({ context, plan: queryPlan }), resolved });
|
|
2449
|
+
}
|
|
2450
|
+
const materialize = config.materialize === true ? {} : config.materialize;
|
|
2451
|
+
const computeName = `${name}__compute`;
|
|
2452
|
+
const tableName = `${name}__materialized`;
|
|
2453
|
+
const compute = defineView(computeName, typedColumns, {
|
|
2454
|
+
policies: { read },
|
|
2455
|
+
query: (context) => buildResolvedQuery({ context, plan: queryPlan }),
|
|
2456
|
+
internal: true
|
|
2457
|
+
});
|
|
2458
|
+
const table = defineTable(tableName, viewColumns, {
|
|
2459
|
+
description: `Materialized rows of resolved table "${name}". System-maintained from "${computeName}".`,
|
|
2460
|
+
internal: true,
|
|
2461
|
+
materializes: name,
|
|
2462
|
+
...materialize.indexes !== void 0 ? { indexes: materialize.indexes } : {}
|
|
2463
|
+
});
|
|
2464
|
+
return defineView(name, typedColumns, {
|
|
2465
|
+
...surface,
|
|
2466
|
+
query: (context) => {
|
|
2467
|
+
const handle = context.relation(table);
|
|
2468
|
+
const selected = Object.fromEntries(Object.entries(viewColumns).map(([property, builder]) => [
|
|
2469
|
+
property,
|
|
2470
|
+
sql`${handle[property]}`.as(builder.columnNameFor(property))
|
|
2471
|
+
]));
|
|
2472
|
+
return context.qb.select(selected).from(context.relation(table));
|
|
2473
|
+
},
|
|
2474
|
+
resolved: {
|
|
2475
|
+
...resolved,
|
|
2476
|
+
internalTables: [...resolved.internalTables, table],
|
|
2477
|
+
internalViews: [compute],
|
|
2478
|
+
materialized: { table: tableName, compute: computeName }
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
function planRules({ config, plans, dataColumns, fail }) {
|
|
2483
|
+
const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
|
|
2484
|
+
const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
|
|
2485
|
+
for (const column of Object.keys(config.resolve ?? {})) {
|
|
2486
|
+
if (!dataColumns.includes(column)) fail(`resolve names unknown column "${column}"`);
|
|
2487
|
+
}
|
|
2488
|
+
return dataColumns.map((column) => {
|
|
2489
|
+
const explicit = config.resolve?.[column];
|
|
2490
|
+
const comparator = explicit ?? fallback;
|
|
2491
|
+
const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name) => plans.find((plan) => plan.name === name)?.columns.includes(column));
|
|
2492
|
+
for (const candidate of candidates) {
|
|
2493
|
+
const plan = plans.find((entry) => entry.name === candidate);
|
|
2494
|
+
if (!plan) fail(`column "${column}" names unknown source "${candidate}"`);
|
|
2495
|
+
else if (!plan.columns.includes(column)) fail(`column "${column}" candidate "${candidate}" does not supply it`);
|
|
2496
|
+
}
|
|
2497
|
+
if (candidates.length === 0) fail(`column "${column}" has no candidate source`);
|
|
2498
|
+
if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail });
|
|
2499
|
+
return {
|
|
2500
|
+
column,
|
|
2501
|
+
comparator: comparator.kind,
|
|
2502
|
+
candidates,
|
|
2503
|
+
...comparator.kind === "custom" && comparator.orderBy !== void 0 ? { orderBy: comparator.orderBy } : {}
|
|
2504
|
+
};
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2508
|
+
"value",
|
|
2509
|
+
"updated_at",
|
|
2510
|
+
"source",
|
|
2511
|
+
"actor",
|
|
2512
|
+
"ord",
|
|
2513
|
+
"asc",
|
|
2514
|
+
"desc",
|
|
2515
|
+
"nulls",
|
|
2516
|
+
"first",
|
|
2517
|
+
"last",
|
|
2518
|
+
"case",
|
|
2519
|
+
"when",
|
|
2520
|
+
"then",
|
|
2521
|
+
"else",
|
|
2522
|
+
"end",
|
|
2523
|
+
"and",
|
|
2524
|
+
"or",
|
|
2525
|
+
"not",
|
|
2526
|
+
"is",
|
|
2527
|
+
"null",
|
|
2528
|
+
"true",
|
|
2529
|
+
"false",
|
|
2530
|
+
"in",
|
|
2531
|
+
"distinct",
|
|
2532
|
+
"from",
|
|
2533
|
+
"coalesce",
|
|
2534
|
+
"nullif",
|
|
2535
|
+
"greatest",
|
|
2536
|
+
"least",
|
|
2537
|
+
"text",
|
|
2538
|
+
"numeric",
|
|
2539
|
+
"integer",
|
|
2540
|
+
"bigint",
|
|
2541
|
+
"boolean",
|
|
2542
|
+
"timestamptz",
|
|
2543
|
+
"date",
|
|
2544
|
+
"jsonb",
|
|
2545
|
+
"interval"
|
|
2546
|
+
]);
|
|
2547
|
+
var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
2548
|
+
var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
|
|
2549
|
+
function assertSafeOrderBy({ orderBy, column, fail }) {
|
|
2550
|
+
ORDER_BY_TOKEN.lastIndex = 0;
|
|
2551
|
+
let consumed = 0;
|
|
2552
|
+
for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
|
|
2553
|
+
consumed = ORDER_BY_TOKEN.lastIndex;
|
|
2554
|
+
const token = match[0].toLowerCase();
|
|
2555
|
+
if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
|
|
2556
|
+
fail(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (consumed !== orderBy.length || orderBy.trim() === "") {
|
|
2560
|
+
fail(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
1472
2563
|
function stableJson(value) {
|
|
1473
2564
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1474
2565
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -1575,8 +2666,19 @@ function defineNamespace(input) {
|
|
|
1575
2666
|
return config;
|
|
1576
2667
|
}
|
|
1577
2668
|
function serializeNamespace(input) {
|
|
1578
|
-
const { config
|
|
1579
|
-
const views = input.views ?? [];
|
|
2669
|
+
const { config } = input;
|
|
2670
|
+
const views = [...input.views ?? []];
|
|
2671
|
+
const tables = [...input.tables];
|
|
2672
|
+
for (const view of views) {
|
|
2673
|
+
for (const internal of view.config.resolved?.internalTables ?? []) {
|
|
2674
|
+
if (!tables.includes(internal)) tables.push(internal);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
for (const view of [...views]) {
|
|
2678
|
+
for (const internal of view.config.resolved?.internalViews ?? []) {
|
|
2679
|
+
if (!views.includes(internal)) views.push(internal);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
1580
2682
|
const enums = input.enums ?? [];
|
|
1581
2683
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
1582
2684
|
for (const table of tables) {
|
|
@@ -1678,6 +2780,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1678
2780
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
1679
2781
|
if (table.source !== void 0) candidate.source = table.source;
|
|
1680
2782
|
if (table.source !== void 0) {
|
|
2783
|
+
if (table.config.checks?.length) {
|
|
2784
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
|
|
2785
|
+
}
|
|
1681
2786
|
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
1682
2787
|
throw new OrmDefinitionError(
|
|
1683
2788
|
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
@@ -1715,6 +2820,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1715
2820
|
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1716
2821
|
candidate.columns?.push(column);
|
|
1717
2822
|
}
|
|
2823
|
+
if (table.config.internal === true) candidate.internal = true;
|
|
2824
|
+
if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
|
|
2825
|
+
if (table.config.cellMetadata === true && table.source !== void 0) {
|
|
2826
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
|
|
2827
|
+
}
|
|
1718
2828
|
if (table.config.primaryKey !== void 0) {
|
|
1719
2829
|
if (columnLevelPk.length > 0) {
|
|
1720
2830
|
throw new OrmDefinitionError(
|
|
@@ -1728,6 +2838,24 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1728
2838
|
} else if (columnLevelPk.length > 0) {
|
|
1729
2839
|
candidate.primaryKey = columnLevelPk;
|
|
1730
2840
|
}
|
|
2841
|
+
if (table.config.cellMetadata === true) {
|
|
2842
|
+
candidate.cellMetadata = true;
|
|
2843
|
+
const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
|
|
2844
|
+
const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
|
|
2845
|
+
for (const column of tracked) {
|
|
2846
|
+
for (const part of parts) {
|
|
2847
|
+
candidate.columns?.push({
|
|
2848
|
+
name: cellColumnName(column.name, part),
|
|
2849
|
+
columnName: cellColumnName(column.columnName, part),
|
|
2850
|
+
type: CELL_PART_TYPES[part],
|
|
2851
|
+
nullable: true,
|
|
2852
|
+
unique: false,
|
|
2853
|
+
deprecated: false,
|
|
2854
|
+
cell: { of: column.name, part }
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
1731
2859
|
if (table.config.foreignKeys?.length) {
|
|
1732
2860
|
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1733
2861
|
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
@@ -1740,10 +2868,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1740
2868
|
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1741
2869
|
target: target.tableName,
|
|
1742
2870
|
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1743
|
-
...fk.deferrable ? { deferrable: fk.deferrable } : {}
|
|
2871
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {},
|
|
2872
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
2873
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
1744
2874
|
};
|
|
1745
2875
|
});
|
|
1746
2876
|
}
|
|
2877
|
+
if (table.config.checks?.length) {
|
|
2878
|
+
candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
|
|
2879
|
+
}
|
|
1747
2880
|
for (const index3 of table.config.indexes ?? []) {
|
|
1748
2881
|
const entry = {
|
|
1749
2882
|
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1839,7 +2972,33 @@ function buildCanonicalView({
|
|
|
1839
2972
|
columns,
|
|
1840
2973
|
...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
|
|
1841
2974
|
links,
|
|
1842
|
-
policies: { read: toCanonicalPolicy(view.config.policies.read) }
|
|
2975
|
+
policies: { read: toCanonicalPolicy(view.config.policies.read) },
|
|
2976
|
+
...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
|
|
2977
|
+
...view.config.internal === true ? { internal: true } : {}
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
function buildCanonicalResolved(resolved) {
|
|
2981
|
+
const policies = {};
|
|
2982
|
+
if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
|
|
2983
|
+
if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
|
|
2984
|
+
if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
|
|
2985
|
+
return {
|
|
2986
|
+
sources: resolved.sources.map((source) => ({
|
|
2987
|
+
name: source.name,
|
|
2988
|
+
kind: source.kind,
|
|
2989
|
+
table: source.table.tableName,
|
|
2990
|
+
columns: [...source.columns],
|
|
2991
|
+
...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
|
|
2992
|
+
...source.key !== void 0 ? { key: source.key } : {},
|
|
2993
|
+
...source.fetch !== void 0 ? { fetch: source.fetch } : {},
|
|
2994
|
+
...source.function !== void 0 ? { function: { ...source.function } } : {},
|
|
2995
|
+
...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
|
|
2996
|
+
...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
|
|
2997
|
+
})),
|
|
2998
|
+
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
2999
|
+
deletedBy: [...resolved.deletedBy],
|
|
3000
|
+
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
3001
|
+
policies
|
|
1843
3002
|
};
|
|
1844
3003
|
}
|
|
1845
3004
|
function buildCanonicalLink({
|
|
@@ -1856,6 +3015,9 @@ function buildCanonicalLink({
|
|
|
1856
3015
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
1857
3016
|
return fail("sets enforce: true, which is only supported on toOne links");
|
|
1858
3017
|
}
|
|
3018
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
|
|
3019
|
+
return fail("sets a referential action without enforce: true");
|
|
3020
|
+
}
|
|
1859
3021
|
if (link.linkKind === "toPlatformUser") {
|
|
1860
3022
|
if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
|
|
1861
3023
|
return {
|
|
@@ -1884,6 +3046,8 @@ function buildCanonicalLink({
|
|
|
1884
3046
|
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1885
3047
|
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
1886
3048
|
enforce: link.enforce,
|
|
3049
|
+
...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
|
|
3050
|
+
...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
|
|
1887
3051
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1888
3052
|
};
|
|
1889
3053
|
}
|
|
@@ -2337,7 +3501,7 @@ var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
|
|
|
2337
3501
|
var IDENTIFIER_START = /[a-z_]/;
|
|
2338
3502
|
var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
|
|
2339
3503
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
2340
|
-
function
|
|
3504
|
+
function tokenize2(source) {
|
|
2341
3505
|
const tokens = [];
|
|
2342
3506
|
let position = 0;
|
|
2343
3507
|
while (position < source.length) {
|
|
@@ -2411,7 +3575,7 @@ function readString(source, start) {
|
|
|
2411
3575
|
}
|
|
2412
3576
|
throw new PredicateParseError("unterminated string literal", start);
|
|
2413
3577
|
}
|
|
2414
|
-
var
|
|
3578
|
+
var Parser2 = class {
|
|
2415
3579
|
constructor(tokens) {
|
|
2416
3580
|
this.tokens = tokens;
|
|
2417
3581
|
}
|
|
@@ -2543,7 +3707,7 @@ var Parser = class {
|
|
|
2543
3707
|
function parsePredicate(source) {
|
|
2544
3708
|
const trimmed = source.trim();
|
|
2545
3709
|
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
2546
|
-
return { source: trimmed, ast: new
|
|
3710
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
2547
3711
|
}
|
|
2548
3712
|
|
|
2549
3713
|
// src/schema/predicates.ts
|
|
@@ -2628,8 +3792,8 @@ var actionSchema = z.strictObject({
|
|
|
2628
3792
|
}
|
|
2629
3793
|
}
|
|
2630
3794
|
});
|
|
2631
|
-
function extractNamedParams(
|
|
2632
|
-
const withoutStrings =
|
|
3795
|
+
function extractNamedParams(sql7) {
|
|
3796
|
+
const withoutStrings = sql7.replace(QUOTED_STRING_PATTERN, "''");
|
|
2633
3797
|
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
2634
3798
|
return [...new Set(names)];
|
|
2635
3799
|
}
|
|
@@ -2696,17 +3860,6 @@ var SQL_TYPE_NAMES = {
|
|
|
2696
3860
|
};
|
|
2697
3861
|
var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
|
|
2698
3862
|
|
|
2699
|
-
// src/compiler/sql.ts
|
|
2700
|
-
var CLAIMS_SETTING = "request.jwt.claims";
|
|
2701
|
-
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
2702
|
-
var ROLES_CLAIM_KEY = "roles";
|
|
2703
|
-
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
2704
|
-
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
2705
|
-
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
2706
|
-
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
2707
|
-
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
2708
|
-
var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
2709
|
-
|
|
2710
3863
|
// src/compiler/predicates.ts
|
|
2711
3864
|
var SQL_COMPARE_OPS = {
|
|
2712
3865
|
"=": "=",
|
|
@@ -2786,8 +3939,8 @@ var actionParamArg = (name) => `p_${name}`;
|
|
|
2786
3939
|
function compileActionStatements(action, renderParam = actionParamArg) {
|
|
2787
3940
|
return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
|
|
2788
3941
|
}
|
|
2789
|
-
function compileStatement(
|
|
2790
|
-
const compiled =
|
|
3942
|
+
function compileStatement(sql7, renderParam) {
|
|
3943
|
+
const compiled = sql7.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
|
|
2791
3944
|
if (claimKey !== void 0) {
|
|
2792
3945
|
if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
|
|
2793
3946
|
return claimExpression(claimKey);
|
|
@@ -2852,10 +4005,10 @@ function physicalColumn(logical, options) {
|
|
|
2852
4005
|
return options.columnNames?.[logical] ?? logical;
|
|
2853
4006
|
}
|
|
2854
4007
|
function compileCmp(op, column, value, options) {
|
|
2855
|
-
const
|
|
2856
|
-
const declared = options.columnTypes?.[
|
|
4008
|
+
const physical2 = physicalColumn(column, options);
|
|
4009
|
+
const declared = options.columnTypes?.[physical2] ?? options.columnTypes?.[column];
|
|
2857
4010
|
const isEnum = typeof declared === "object" && declared !== null;
|
|
2858
|
-
const col = isEnum ? `${quoteIdentifier(
|
|
4011
|
+
const col = isEnum ? `${quoteIdentifier(physical2)}::text` : quoteIdentifier(physical2);
|
|
2859
4012
|
const columnType = isEnum ? "text" : declared;
|
|
2860
4013
|
switch (op) {
|
|
2861
4014
|
case "eq":
|
|
@@ -2899,11 +4052,11 @@ function compileEquality(col, value, columnType, negated, options) {
|
|
|
2899
4052
|
function compileValue(value, columnType, _options) {
|
|
2900
4053
|
switch (value.kind) {
|
|
2901
4054
|
case "literal": {
|
|
2902
|
-
const
|
|
2903
|
-
if (
|
|
2904
|
-
if (typeof
|
|
2905
|
-
if (typeof
|
|
2906
|
-
return String(
|
|
4055
|
+
const literal2 = value.value;
|
|
4056
|
+
if (literal2 === null) return "NULL";
|
|
4057
|
+
if (typeof literal2 === "string") return quoteLiteral(literal2);
|
|
4058
|
+
if (typeof literal2 === "boolean") return literal2 ? "TRUE" : "FALSE";
|
|
4059
|
+
return String(literal2);
|
|
2907
4060
|
}
|
|
2908
4061
|
case "claim": {
|
|
2909
4062
|
const expression = claimExpression(value.key);
|
|
@@ -2952,8 +4105,8 @@ function actionPrepareChecks(definition) {
|
|
|
2952
4105
|
const order = actionParamOrder(action);
|
|
2953
4106
|
const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
|
|
2954
4107
|
const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
|
|
2955
|
-
compiled.forEach((
|
|
2956
|
-
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns,
|
|
4108
|
+
compiled.forEach((sql7, index3) => {
|
|
4109
|
+
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql7) : sql7;
|
|
2957
4110
|
statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
|
|
2958
4111
|
});
|
|
2959
4112
|
}
|
|
@@ -3335,14 +4488,14 @@ function comparisonTypeIssue(left, right, columns) {
|
|
|
3335
4488
|
return null;
|
|
3336
4489
|
}
|
|
3337
4490
|
const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
|
|
3338
|
-
const
|
|
3339
|
-
if (!column || !
|
|
4491
|
+
const literal2 = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
|
|
4492
|
+
if (!column || !literal2 || literal2.value === null) return null;
|
|
3340
4493
|
const columnGroup = groupOf(column);
|
|
3341
4494
|
if (!columnGroup) return null;
|
|
3342
|
-
if (typeof
|
|
3343
|
-
const literalGroup = typeof
|
|
4495
|
+
if (typeof literal2.value === "string") return null;
|
|
4496
|
+
const literalGroup = typeof literal2.value === "number" ? "numeric" : "boolean";
|
|
3344
4497
|
if (literalGroup !== columnGroup) {
|
|
3345
|
-
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof
|
|
4498
|
+
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal2.value} literal \u2014 incomparable types`;
|
|
3346
4499
|
}
|
|
3347
4500
|
return null;
|
|
3348
4501
|
}
|
|
@@ -3428,8 +4581,8 @@ function syncedViewStatements({
|
|
|
3428
4581
|
const backing = backingMap.get(table.table);
|
|
3429
4582
|
if (!backing) continue;
|
|
3430
4583
|
const viewName = `${schema}.${quoteIdentifier(table.table)}`;
|
|
3431
|
-
const
|
|
3432
|
-
const physicalTarget = `${quoteIdentifier(
|
|
4584
|
+
const physical2 = splitPhysicalRelation(backing.physicalRelation);
|
|
4585
|
+
const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
|
|
3433
4586
|
const columnTypes = mergeSyncedViewColumnTypes({
|
|
3434
4587
|
table,
|
|
3435
4588
|
canonicalColumnTypes: columnTypesByTable?.get(table.table),
|
|
@@ -3443,7 +4596,7 @@ function syncedViewStatements({
|
|
|
3443
4596
|
columnNames: columnNamesByTable?.get(table.table)
|
|
3444
4597
|
})}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
|
|
3445
4598
|
if (backingPrivileges === "grant") {
|
|
3446
|
-
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(
|
|
4599
|
+
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical2.schema)} TO ${owner}`);
|
|
3447
4600
|
statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
|
|
3448
4601
|
}
|
|
3449
4602
|
statements.push(
|
|
@@ -3464,10 +4617,10 @@ function mergeSyncedViewColumnTypes({
|
|
|
3464
4617
|
if (def?.type) fromTable[name] = def.type;
|
|
3465
4618
|
}
|
|
3466
4619
|
if (columnNames) {
|
|
3467
|
-
for (const [logical,
|
|
3468
|
-
const declared = fromTable[
|
|
4620
|
+
for (const [logical, physical2] of Object.entries(columnNames)) {
|
|
4621
|
+
const declared = fromTable[physical2] ?? fromTable[logical];
|
|
3469
4622
|
if (!declared) continue;
|
|
3470
|
-
fromTable[
|
|
4623
|
+
fromTable[physical2] = declared;
|
|
3471
4624
|
fromTable[logical] = declared;
|
|
3472
4625
|
}
|
|
3473
4626
|
}
|
|
@@ -3506,8 +4659,8 @@ function syncedBackingRevokeStatements({
|
|
|
3506
4659
|
const seen = /* @__PURE__ */ new Set();
|
|
3507
4660
|
const statements = [];
|
|
3508
4661
|
for (const backing of backings) {
|
|
3509
|
-
const
|
|
3510
|
-
const physicalTarget = `${quoteIdentifier(
|
|
4662
|
+
const physical2 = splitPhysicalRelation(backing.physicalRelation);
|
|
4663
|
+
const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
|
|
3511
4664
|
if (seen.has(physicalTarget)) continue;
|
|
3512
4665
|
seen.add(physicalTarget);
|
|
3513
4666
|
statements.push(`REVOKE SELECT ON ${physicalTarget} FROM ${owner}`);
|
|
@@ -3717,6 +4870,90 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
3717
4870
|
const tables = snapshot.tables ?? {};
|
|
3718
4871
|
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
3719
4872
|
}
|
|
4873
|
+
|
|
4874
|
+
// src/engine/materialized-refresh.ts
|
|
4875
|
+
function materializedRefreshFunctions(canonical) {
|
|
4876
|
+
const schema = ormSchemaName(canonical.namespace);
|
|
4877
|
+
const functions = /* @__PURE__ */ new Map();
|
|
4878
|
+
for (const view of canonical.views ?? []) {
|
|
4879
|
+
const materialized = view.resolved?.materialized;
|
|
4880
|
+
if (!materialized) continue;
|
|
4881
|
+
const table = canonical.tables.find((candidate) => candidate.table === materialized.table);
|
|
4882
|
+
const pk = view.primaryKey ?? [];
|
|
4883
|
+
if (!table || pk.length === 0) {
|
|
4884
|
+
throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
|
|
4885
|
+
}
|
|
4886
|
+
functions.set(view.view, refreshFunctionSql({
|
|
4887
|
+
schema,
|
|
4888
|
+
name: view.view,
|
|
4889
|
+
table: materialized.table,
|
|
4890
|
+
compute: materialized.compute,
|
|
4891
|
+
columns: table.columns.map((column) => column.columnName),
|
|
4892
|
+
pk
|
|
4893
|
+
}));
|
|
4894
|
+
}
|
|
4895
|
+
return functions;
|
|
4896
|
+
}
|
|
4897
|
+
function refreshFunctionSql({ schema, name, table, compute, columns, pk }) {
|
|
4898
|
+
const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name))}`;
|
|
4899
|
+
const target = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
|
4900
|
+
const source = `${quoteIdentifier(schema)}.${quoteIdentifier(compute)}`;
|
|
4901
|
+
const list = columns.map(quoteIdentifier).join(", ");
|
|
4902
|
+
const others = columns.filter((column) => !pk.includes(column));
|
|
4903
|
+
const keys = pk.map(quoteIdentifier).join(", ");
|
|
4904
|
+
const matches = pk.map((column) => `c.${quoteIdentifier(column)} = m.${quoteIdentifier(column)}`).join(" AND ");
|
|
4905
|
+
const onConflict = others.length === 0 ? "DO NOTHING" : `DO UPDATE SET ${others.map((column) => `${quoteIdentifier(column)} = EXCLUDED.${quoteIdentifier(column)}`).join(", ")}
|
|
4906
|
+
WHERE (${others.map((column) => `m.${quoteIdentifier(column)}`).join(", ")}) IS DISTINCT FROM (${others.map((column) => `EXCLUDED.${quoteIdentifier(column)}`).join(", ")})`;
|
|
4907
|
+
const lockKey = quoteLiteral(materializeLockKey(schema, name));
|
|
4908
|
+
return `CREATE OR REPLACE FUNCTION ${fn}(wait boolean DEFAULT false)
|
|
4909
|
+
RETURNS TABLE(upserted bigint, deleted bigint, locked boolean)
|
|
4910
|
+
LANGUAGE plpgsql
|
|
4911
|
+
SECURITY INVOKER
|
|
4912
|
+
SET search_path = pg_catalog, pg_temp
|
|
4913
|
+
AS $orm_refresh$
|
|
4914
|
+
#variable_conflict use_column
|
|
4915
|
+
DECLARE
|
|
4916
|
+
caller_claims text := current_setting('request.jwt.claims', true);
|
|
4917
|
+
BEGIN
|
|
4918
|
+
IF wait THEN
|
|
4919
|
+
PERFORM pg_advisory_xact_lock(hashtext(${lockKey}));
|
|
4920
|
+
ELSIF NOT pg_try_advisory_xact_lock(hashtext(${lockKey})) THEN
|
|
4921
|
+
RETURN QUERY SELECT 0::bigint, 0::bigint, false;
|
|
4922
|
+
RETURN;
|
|
4923
|
+
END IF;
|
|
4924
|
+
PERFORM set_config('request.jwt.claims', ${quoteLiteral(MATERIALIZE_CLAIMS_JSON)}, true);
|
|
4925
|
+
RETURN QUERY
|
|
4926
|
+
WITH up AS (
|
|
4927
|
+
INSERT INTO ${target} AS m (${list})
|
|
4928
|
+
SELECT ${list} FROM ${source}
|
|
4929
|
+
ON CONFLICT (${keys}) ${onConflict}
|
|
4930
|
+
RETURNING 1
|
|
4931
|
+
), gone AS (
|
|
4932
|
+
DELETE FROM ${target} AS m
|
|
4933
|
+
WHERE NOT EXISTS (SELECT 1 FROM ${source} AS c WHERE ${matches})
|
|
4934
|
+
RETURNING 1
|
|
4935
|
+
)
|
|
4936
|
+
SELECT (SELECT count(*) FROM up), (SELECT count(*) FROM gone), true;
|
|
4937
|
+
-- NULL resets to the session value when the caller had none.
|
|
4938
|
+
PERFORM set_config('request.jwt.claims', caller_claims, true);
|
|
4939
|
+
END
|
|
4940
|
+
$orm_refresh$`;
|
|
4941
|
+
}
|
|
4942
|
+
function refreshFunctionStatements({ namespace, previous, current }) {
|
|
4943
|
+
const schema = quoteIdentifier(ormSchemaName(namespace));
|
|
4944
|
+
const statements = [];
|
|
4945
|
+
for (const name of Object.keys(previous).sort()) {
|
|
4946
|
+
if (!current.has(name)) statements.push(`DROP FUNCTION IF EXISTS ${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`);
|
|
4947
|
+
}
|
|
4948
|
+
for (const [name, create] of [...current].sort(([a], [b]) => a.localeCompare(b))) {
|
|
4949
|
+
if (previous[name] === create) continue;
|
|
4950
|
+
const fn = `${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`;
|
|
4951
|
+
statements.push(create, `REVOKE ALL ON FUNCTION ${fn} FROM PUBLIC`);
|
|
4952
|
+
}
|
|
4953
|
+
return statements;
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
// src/engine/canonical2.ts
|
|
3720
4957
|
var FACTORIES2 = {
|
|
3721
4958
|
uuid: (name) => uuid(name),
|
|
3722
4959
|
text: (name) => text(name),
|
|
@@ -3829,18 +5066,18 @@ function rewriteSyncedSourceRelationsInViewQuery({
|
|
|
3829
5066
|
return rewritten;
|
|
3830
5067
|
});
|
|
3831
5068
|
}
|
|
3832
|
-
function transformSqlOutsideLiterals(
|
|
5069
|
+
function transformSqlOutsideLiterals(sql7, transform) {
|
|
3833
5070
|
const out = [];
|
|
3834
5071
|
let state = { kind: "normal" };
|
|
3835
5072
|
let i = 0;
|
|
3836
5073
|
let codeStart = 0;
|
|
3837
5074
|
const emitCode = (end) => {
|
|
3838
|
-
if (end > codeStart) out.push(transform(
|
|
5075
|
+
if (end > codeStart) out.push(transform(sql7.slice(codeStart, end)));
|
|
3839
5076
|
codeStart = end;
|
|
3840
5077
|
};
|
|
3841
|
-
while (i <
|
|
3842
|
-
const c =
|
|
3843
|
-
const next = i + 1 <
|
|
5078
|
+
while (i < sql7.length) {
|
|
5079
|
+
const c = sql7[i];
|
|
5080
|
+
const next = i + 1 < sql7.length ? sql7[i + 1] : "";
|
|
3844
5081
|
switch (state.kind) {
|
|
3845
5082
|
case "normal": {
|
|
3846
5083
|
if (c === "-" && next === "-") {
|
|
@@ -3860,7 +5097,7 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3860
5097
|
break;
|
|
3861
5098
|
}
|
|
3862
5099
|
if ((c === "E" || c === "e") && next === "'") {
|
|
3863
|
-
const prev = i > 0 ?
|
|
5100
|
+
const prev = i > 0 ? sql7[i - 1] : "";
|
|
3864
5101
|
if (!/[A-Za-z0-9_]/.test(prev)) {
|
|
3865
5102
|
emitCode(i);
|
|
3866
5103
|
state = { kind: "e-single" };
|
|
@@ -3879,9 +5116,9 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3879
5116
|
break;
|
|
3880
5117
|
}
|
|
3881
5118
|
if (c === "$") {
|
|
3882
|
-
const prev = i > 0 ?
|
|
5119
|
+
const prev = i > 0 ? sql7[i - 1] : "";
|
|
3883
5120
|
if (!/[A-Za-z0-9_$]/.test(prev)) {
|
|
3884
|
-
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(
|
|
5121
|
+
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql7.slice(i));
|
|
3885
5122
|
if (tagMatch) {
|
|
3886
5123
|
emitCode(i);
|
|
3887
5124
|
state = { kind: "dollar", tag: tagMatch[1] ?? "" };
|
|
@@ -3913,8 +5150,8 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3913
5150
|
break;
|
|
3914
5151
|
}
|
|
3915
5152
|
case "e-single": {
|
|
3916
|
-
if (c === "\\" && i + 1 <
|
|
3917
|
-
out.push(c,
|
|
5153
|
+
if (c === "\\" && i + 1 < sql7.length) {
|
|
5154
|
+
out.push(c, sql7[i + 1]);
|
|
3918
5155
|
i += 2;
|
|
3919
5156
|
break;
|
|
3920
5157
|
}
|
|
@@ -3936,7 +5173,7 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3936
5173
|
}
|
|
3937
5174
|
case "dollar": {
|
|
3938
5175
|
const closer = `$${state.tag}$`;
|
|
3939
|
-
if (
|
|
5176
|
+
if (sql7.startsWith(closer, i)) {
|
|
3940
5177
|
out.push(closer);
|
|
3941
5178
|
state = { kind: "normal" };
|
|
3942
5179
|
i += closer.length;
|
|
@@ -3980,15 +5217,15 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3980
5217
|
}
|
|
3981
5218
|
}
|
|
3982
5219
|
}
|
|
3983
|
-
emitCode(
|
|
5220
|
+
emitCode(sql7.length);
|
|
3984
5221
|
return out.join("");
|
|
3985
5222
|
}
|
|
3986
5223
|
function escapeRegExp(value) {
|
|
3987
5224
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3988
5225
|
}
|
|
3989
|
-
function sqlReferencesQualifiedRelation(
|
|
5226
|
+
function sqlReferencesQualifiedRelation(sql7, sourceSchema, sourceName) {
|
|
3990
5227
|
let referenced = false;
|
|
3991
|
-
transformSqlOutsideLiterals(
|
|
5228
|
+
transformSqlOutsideLiterals(sql7, (segment) => {
|
|
3992
5229
|
if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
|
|
3993
5230
|
return segment;
|
|
3994
5231
|
});
|
|
@@ -3999,16 +5236,18 @@ function qualifiedRelationPattern(sourceSchema, sourceName) {
|
|
|
3999
5236
|
const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
|
|
4000
5237
|
return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
|
|
4001
5238
|
}
|
|
4002
|
-
function replaceQualifiedRelation(
|
|
4003
|
-
return
|
|
5239
|
+
function replaceQualifiedRelation(sql7, sourceSchema, sourceName, boundSchema, boundTable) {
|
|
5240
|
+
return sql7.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
|
|
4004
5241
|
}
|
|
4005
5242
|
function generateSnapshotV2(definition, previousId3) {
|
|
4006
5243
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
4007
5244
|
const snapshot = generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
4008
5245
|
if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
|
|
4009
5246
|
const extended = snapshot;
|
|
5247
|
+
const refreshFunctions = materializedRefreshFunctions(definition);
|
|
4010
5248
|
extended._sequenceOrm = {
|
|
4011
5249
|
...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
|
|
5250
|
+
...refreshFunctions.size > 0 ? { refreshFunctions: Object.fromEntries(refreshFunctions) } : {},
|
|
4012
5251
|
viewDependencies: Object.fromEntries(
|
|
4013
5252
|
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
4014
5253
|
)
|
|
@@ -4048,15 +5287,19 @@ function buildTable2(schema, table, built, enums, byName, introspection) {
|
|
|
4048
5287
|
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
4049
5288
|
...buildCompositePrimaryKey(table, primaryKeyColumns, self),
|
|
4050
5289
|
...buildIndexes2(table, self),
|
|
5290
|
+
...(table.checks ?? []).map((check$1) => check(check$1.name, sql.raw(compileCheckExpression(check$1.expression, table.columns)))),
|
|
4051
5291
|
...buildForeignKeys2(table, self, built, byName),
|
|
4052
5292
|
...(table.foreignKeys ?? []).map((fk) => {
|
|
4053
5293
|
const target = fk.target === table.table ? self : built.get(fk.target);
|
|
4054
5294
|
if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
|
|
4055
|
-
|
|
5295
|
+
let constraint = foreignKey({
|
|
4056
5296
|
name: fk.name,
|
|
4057
5297
|
columns: fk.columns.map((column) => self[column]),
|
|
4058
5298
|
foreignColumns: fk.references.map((column) => target[column])
|
|
4059
5299
|
});
|
|
5300
|
+
if (fk.onDelete !== void 0) constraint = constraint.onDelete(fk.onDelete);
|
|
5301
|
+
if (fk.onUpdate !== void 0) constraint = constraint.onUpdate(fk.onUpdate);
|
|
5302
|
+
return constraint;
|
|
4060
5303
|
})
|
|
4061
5304
|
]);
|
|
4062
5305
|
return drizzleTable.enableRLS();
|
|
@@ -4147,11 +5390,14 @@ function buildForeignKeys2(table, self, built, byName) {
|
|
|
4147
5390
|
`enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
|
|
4148
5391
|
);
|
|
4149
5392
|
}
|
|
4150
|
-
|
|
5393
|
+
let constraint = foreignKey({
|
|
4151
5394
|
name: foreignKeyName(table.table, link.name),
|
|
4152
5395
|
columns: [sourceColumn],
|
|
4153
5396
|
foreignColumns: [targetColumn]
|
|
4154
5397
|
});
|
|
5398
|
+
if (link.onDelete !== void 0) constraint = constraint.onDelete(link.onDelete);
|
|
5399
|
+
if (link.onUpdate !== void 0) constraint = constraint.onUpdate(link.onUpdate);
|
|
5400
|
+
return constraint;
|
|
4155
5401
|
});
|
|
4156
5402
|
}
|
|
4157
5403
|
var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
|
|
@@ -4272,9 +5518,23 @@ function findTable2(snapshot, schema, table) {
|
|
|
4272
5518
|
const tables = snapshot.tables ?? {};
|
|
4273
5519
|
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
4274
5520
|
}
|
|
4275
|
-
|
|
5521
|
+
function planMigrationV2(input) {
|
|
5522
|
+
return planWithViewRecreation({ ...input, viewRecreation: "affected" });
|
|
5523
|
+
}
|
|
5524
|
+
async function planCommittedMigrationV2({
|
|
4276
5525
|
canonical,
|
|
4277
|
-
previousSnapshot
|
|
5526
|
+
previousSnapshot,
|
|
5527
|
+
reproduces
|
|
5528
|
+
}) {
|
|
5529
|
+
const current = await planMigrationV2({ canonical, previousSnapshot });
|
|
5530
|
+
if (reproduces(current)) return { plan: current, reproduced: true };
|
|
5531
|
+
const legacy = await planWithViewRecreation({ canonical, previousSnapshot, viewRecreation: "all-on-base-change" });
|
|
5532
|
+
return reproduces(legacy) ? { plan: legacy, reproduced: true } : { plan: current, reproduced: false };
|
|
5533
|
+
}
|
|
5534
|
+
async function planWithViewRecreation({
|
|
5535
|
+
canonical,
|
|
5536
|
+
previousSnapshot,
|
|
5537
|
+
viewRecreation
|
|
4278
5538
|
}) {
|
|
4279
5539
|
const schema = ormSchemaName(canonical.namespace);
|
|
4280
5540
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
@@ -4292,7 +5552,14 @@ async function planMigrationV2({
|
|
|
4292
5552
|
currentViews,
|
|
4293
5553
|
previousDependencies,
|
|
4294
5554
|
currentDependencies,
|
|
4295
|
-
|
|
5555
|
+
reach: viewReach({
|
|
5556
|
+
viewRecreation,
|
|
5557
|
+
hintStatements: hints.statements,
|
|
5558
|
+
baseDdl,
|
|
5559
|
+
previous,
|
|
5560
|
+
adjusted: hints.adjusted,
|
|
5561
|
+
current: snapshot
|
|
5562
|
+
})
|
|
4296
5563
|
});
|
|
4297
5564
|
const dropOrder = topologicalViewNames(
|
|
4298
5565
|
new Set([...affected].filter((name) => previousViews.has(name))),
|
|
@@ -4319,13 +5586,18 @@ async function planMigrationV2({
|
|
|
4319
5586
|
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
4320
5587
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
4321
5588
|
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
4322
|
-
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((
|
|
5589
|
+
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql7) => ({ kind: "ddl", sql: sql7 })),
|
|
4323
5590
|
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
4324
5591
|
...createOrder.map((name) => {
|
|
4325
5592
|
const view = currentViewDefinitions.get(name);
|
|
4326
5593
|
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
|
|
4327
5594
|
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
4328
|
-
})
|
|
5595
|
+
}),
|
|
5596
|
+
...refreshFunctionStatements({
|
|
5597
|
+
namespace: canonical.namespace,
|
|
5598
|
+
previous: previous._sequenceOrm?.refreshFunctions ?? {},
|
|
5599
|
+
current: materializedRefreshFunctions(canonical)
|
|
5600
|
+
}).map((sql7) => ({ kind: "ddl", sql: sql7 }))
|
|
4329
5601
|
];
|
|
4330
5602
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
4331
5603
|
}
|
|
@@ -4369,9 +5641,9 @@ function validateViewCatalogStatement({
|
|
|
4369
5641
|
}
|
|
4370
5642
|
const expectedDependencies = sqlTextArray(
|
|
4371
5643
|
view.dependencies.map((dependency) => {
|
|
4372
|
-
const
|
|
4373
|
-
if (!
|
|
4374
|
-
return
|
|
5644
|
+
const physical2 = relationIdentity.get(dependency);
|
|
5645
|
+
if (!physical2) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
5646
|
+
return physical2;
|
|
4375
5647
|
}).sort()
|
|
4376
5648
|
);
|
|
4377
5649
|
const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
@@ -4479,20 +5751,102 @@ function withoutViews(snapshot) {
|
|
|
4479
5751
|
delete clone._sequenceOrm;
|
|
4480
5752
|
return clone;
|
|
4481
5753
|
}
|
|
5754
|
+
var DROPS_TYPE = /^\s*DROP\s+TYPE\b/i;
|
|
5755
|
+
function viewReach({
|
|
5756
|
+
viewRecreation,
|
|
5757
|
+
hintStatements,
|
|
5758
|
+
baseDdl,
|
|
5759
|
+
previous,
|
|
5760
|
+
adjusted,
|
|
5761
|
+
current
|
|
5762
|
+
}) {
|
|
5763
|
+
if (hintStatements.length === 0 && baseDdl.length === 0) return /* @__PURE__ */ new Set();
|
|
5764
|
+
if (viewRecreation === "all-on-base-change") return "all";
|
|
5765
|
+
if (baseDdl.some((statement) => DROPS_TYPE.test(statement))) return "all";
|
|
5766
|
+
const dependencies = snapshotViewDependencies(adjusted);
|
|
5767
|
+
if ([...snapshotViews(adjusted).keys()].some((name) => dependencies[name] === void 0)) return "all";
|
|
5768
|
+
return baseChangesReachingViews({ previous, adjusted, current });
|
|
5769
|
+
}
|
|
5770
|
+
var SNAPSHOT_BOOKKEEPING = /* @__PURE__ */ new Set(["id", "prevId", "version", "dialect", "_meta", "views", "_sequenceOrm", "enums"]);
|
|
5771
|
+
var VIEW_SAFE_TABLE_FIELDS = /* @__PURE__ */ new Set([
|
|
5772
|
+
"indexes",
|
|
5773
|
+
"foreignKeys",
|
|
5774
|
+
"uniqueConstraints",
|
|
5775
|
+
"checkConstraints",
|
|
5776
|
+
"policies",
|
|
5777
|
+
"isRLSEnabled"
|
|
5778
|
+
]);
|
|
5779
|
+
var VIEW_SAFE_COLUMN_FIELDS = /* @__PURE__ */ new Set(["notNull", "default", "isUnique", "uniqueName", "nullsNotDistinct"]);
|
|
5780
|
+
function baseChangesReachingViews({
|
|
5781
|
+
previous,
|
|
5782
|
+
adjusted,
|
|
5783
|
+
current
|
|
5784
|
+
}) {
|
|
5785
|
+
const before = adjusted;
|
|
5786
|
+
const after = current;
|
|
5787
|
+
for (const section of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
5788
|
+
if (SNAPSHOT_BOOKKEEPING.has(section) || section === "tables") continue;
|
|
5789
|
+
if (stableJson(before[section]) !== stableJson(after[section])) return "all";
|
|
5790
|
+
}
|
|
5791
|
+
const relations = /* @__PURE__ */ new Set();
|
|
5792
|
+
const previousTables = snapshotTables(previous);
|
|
5793
|
+
const adjustedTables = snapshotTables(adjusted);
|
|
5794
|
+
for (const key of changedEntryKeys(previousTables, adjustedTables)) {
|
|
5795
|
+
const table = previousTables[key] ?? adjustedTables[key];
|
|
5796
|
+
if (table) relations.add(table.name);
|
|
5797
|
+
}
|
|
5798
|
+
const currentTables = snapshotTables(current);
|
|
5799
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(adjustedTables), ...Object.keys(currentTables)])) {
|
|
5800
|
+
const was = adjustedTables[key];
|
|
5801
|
+
const is2 = currentTables[key];
|
|
5802
|
+
if (!was || !is2) {
|
|
5803
|
+
const table = was ?? is2;
|
|
5804
|
+
if (table) relations.add(table.name);
|
|
5805
|
+
} else if (tableChangeReachesViews({ was, is: is2 })) {
|
|
5806
|
+
relations.add(is2.name);
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
return relations;
|
|
5810
|
+
}
|
|
5811
|
+
function tableChangeReachesViews({ was, is: is2 }) {
|
|
5812
|
+
for (const field of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(is2)])) {
|
|
5813
|
+
if (field === "columns" || VIEW_SAFE_TABLE_FIELDS.has(field)) continue;
|
|
5814
|
+
if (stableJson(was[field]) !== stableJson(is2[field])) return true;
|
|
5815
|
+
}
|
|
5816
|
+
return Object.entries(was.columns).some(([name, column]) => {
|
|
5817
|
+
const next = is2.columns[name];
|
|
5818
|
+
if (!next) return true;
|
|
5819
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(column), ...Object.keys(next)])].some(
|
|
5820
|
+
(field) => !VIEW_SAFE_COLUMN_FIELDS.has(field) && stableJson(column[field]) !== stableJson(next[field])
|
|
5821
|
+
);
|
|
5822
|
+
});
|
|
5823
|
+
}
|
|
5824
|
+
function changedEntryKeys(before, after) {
|
|
5825
|
+
return new Set(
|
|
5826
|
+
[.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter(
|
|
5827
|
+
(key) => stableJson(before[key]) !== stableJson(after[key])
|
|
5828
|
+
)
|
|
5829
|
+
);
|
|
5830
|
+
}
|
|
5831
|
+
function snapshotTables(snapshot) {
|
|
5832
|
+
return snapshot.tables ?? {};
|
|
5833
|
+
}
|
|
4482
5834
|
function affectedViewNames({
|
|
4483
5835
|
previousViews,
|
|
4484
5836
|
currentViews,
|
|
4485
5837
|
previousDependencies,
|
|
4486
5838
|
currentDependencies,
|
|
4487
|
-
|
|
5839
|
+
reach
|
|
4488
5840
|
}) {
|
|
4489
5841
|
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
4490
|
-
if (
|
|
5842
|
+
if (reach === "all") return allNames;
|
|
5843
|
+
const dependenciesOf = (name) => /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
|
|
4491
5844
|
const affected = new Set(
|
|
4492
5845
|
[...allNames].filter((name) => {
|
|
4493
5846
|
const before = previousViews.get(name);
|
|
4494
5847
|
const after = currentViews.get(name);
|
|
4495
|
-
|
|
5848
|
+
if (before === void 0 || after === void 0 || stableJson(before) !== stableJson(after)) return true;
|
|
5849
|
+
return [...dependenciesOf(name)].some((dependency) => reach.has(dependency));
|
|
4496
5850
|
})
|
|
4497
5851
|
);
|
|
4498
5852
|
let grew = true;
|
|
@@ -4500,8 +5854,7 @@ function affectedViewNames({
|
|
|
4500
5854
|
grew = false;
|
|
4501
5855
|
for (const name of allNames) {
|
|
4502
5856
|
if (affected.has(name)) continue;
|
|
4503
|
-
|
|
4504
|
-
if ([...dependencies].some((dependency) => affected.has(dependency))) {
|
|
5857
|
+
if ([...dependenciesOf(name)].some((dependency) => affected.has(dependency))) {
|
|
4505
5858
|
affected.add(name);
|
|
4506
5859
|
grew = true;
|
|
4507
5860
|
}
|
|
@@ -4739,15 +6092,15 @@ async function planMigration({
|
|
|
4739
6092
|
const ddl = await diffStatements(hints.adjusted, snapshot);
|
|
4740
6093
|
const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
|
|
4741
6094
|
const statements = [
|
|
4742
|
-
...policyDrops.map((
|
|
4743
|
-
...hints.statements.map((
|
|
4744
|
-
...ddl.map((
|
|
4745
|
-
...securityStatements(definition).map((
|
|
6095
|
+
...policyDrops.map((sql7) => ({ kind: "security", sql: sql7 })),
|
|
6096
|
+
...hints.statements.map((sql7) => ({ kind: "hint", sql: sql7 })),
|
|
6097
|
+
...ddl.map((sql7) => ({ kind: "ddl", sql: sql7.replace(/;\s*$/, "") })),
|
|
6098
|
+
...securityStatements(definition).map((sql7) => ({ kind: "security", sql: sql7 })),
|
|
4746
6099
|
// PREPARE each action's executes first (plpgsql bodies don't check columns
|
|
4747
6100
|
// at CREATE), then create the functions. Both reference the tables the DDL
|
|
4748
6101
|
// above creates, so they close the plan.
|
|
4749
|
-
...actionPrepareChecks(definition).map((
|
|
4750
|
-
...actionFunctionStatements(definition).map((
|
|
6102
|
+
...actionPrepareChecks(definition).map((sql7) => ({ kind: "function", sql: sql7 })),
|
|
6103
|
+
...actionFunctionStatements(definition).map((sql7) => ({ kind: "function", sql: sql7 }))
|
|
4751
6104
|
];
|
|
4752
6105
|
return { namespace: definition.namespace, schema, statements, snapshot };
|
|
4753
6106
|
}
|
|
@@ -4877,6 +6230,21 @@ var selectionIrSchema = z.lazy(
|
|
|
4877
6230
|
() => z.object({
|
|
4878
6231
|
typename: z.string(),
|
|
4879
6232
|
columns: z.array(z.object({ alias: z.string(), column: z.string() })),
|
|
6233
|
+
cells: z.object({
|
|
6234
|
+
alias: z.string(),
|
|
6235
|
+
typename: z.string(),
|
|
6236
|
+
fields: z.array(z.object({
|
|
6237
|
+
alias: z.string(),
|
|
6238
|
+
column: z.string(),
|
|
6239
|
+
typename: z.string(),
|
|
6240
|
+
parts: z.array(z.object({
|
|
6241
|
+
alias: z.string(),
|
|
6242
|
+
part: z.enum(["source", "at", "by", "conflicts"]),
|
|
6243
|
+
fields: z.array(z.object({ alias: z.string(), field: z.enum(["source", "at", "value"]) })).optional(),
|
|
6244
|
+
typename: z.string().optional()
|
|
6245
|
+
}))
|
|
6246
|
+
}))
|
|
6247
|
+
}).optional(),
|
|
4880
6248
|
relations: z.array(z.object({
|
|
4881
6249
|
alias: z.string(),
|
|
4882
6250
|
link: z.string(),
|
|
@@ -4898,6 +6266,11 @@ var selectionIrSchema = z.lazy(
|
|
|
4898
6266
|
})
|
|
4899
6267
|
);
|
|
4900
6268
|
var includeIfSchema = z.object({ variable: z.string(), negated: z.boolean() });
|
|
6269
|
+
var prefetchPlanIrSchema = z.object({
|
|
6270
|
+
source: z.string(),
|
|
6271
|
+
maxStalenessMs: z.number().int().positive().optional(),
|
|
6272
|
+
key: z.union([z.object({ one: valueIrSchema }), z.object({ many: valueIrSchema })])
|
|
6273
|
+
});
|
|
4901
6274
|
var selectRootSchema = z.object({
|
|
4902
6275
|
kind: z.literal("select"),
|
|
4903
6276
|
alias: z.string(),
|
|
@@ -4909,7 +6282,8 @@ var selectRootSchema = z.object({
|
|
|
4909
6282
|
distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
|
|
4910
6283
|
limit: limitIrSchema,
|
|
4911
6284
|
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
4912
|
-
includeIf: includeIfSchema.optional()
|
|
6285
|
+
includeIf: includeIfSchema.optional(),
|
|
6286
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
4913
6287
|
});
|
|
4914
6288
|
var byPkRootSchema = z.object({
|
|
4915
6289
|
kind: z.literal("byPk"),
|
|
@@ -4919,7 +6293,8 @@ var byPkRootSchema = z.object({
|
|
|
4919
6293
|
pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
|
|
4920
6294
|
/** Policy predicate — ANDed with the PK match. */
|
|
4921
6295
|
where: predIrSchema,
|
|
4922
|
-
includeIf: includeIfSchema.optional()
|
|
6296
|
+
includeIf: includeIfSchema.optional(),
|
|
6297
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
4923
6298
|
});
|
|
4924
6299
|
var aggregateRootSchema = z.object({
|
|
4925
6300
|
kind: z.literal("aggregate"),
|
|
@@ -4977,7 +6352,20 @@ var mutationRootSchema = z.object({
|
|
|
4977
6352
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
4978
6353
|
* row and carry the write policy in `where` like update/delete.
|
|
4979
6354
|
*/
|
|
4980
|
-
op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
6355
|
+
op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
|
|
6356
|
+
/**
|
|
6357
|
+
* resolvedWrite: a write to a resolved table (`table` is its view). The
|
|
6358
|
+
* executor gates on the resolved row (write policy in `where`), writes only
|
|
6359
|
+
* the touched cells into the manual source, then re-checks `insertCheck`
|
|
6360
|
+
* against the resolved row. `revert` clears `revertColumns` from the manual
|
|
6361
|
+
* source; `delete` tombstones the row there.
|
|
6362
|
+
*/
|
|
6363
|
+
resolvedAction: z.enum(["insert", "update", "delete", "revert"]).optional(),
|
|
6364
|
+
revertColumns: z.array(z.string()).optional(),
|
|
6365
|
+
/** `@writeback(source:, mode:)` directives on this root. */
|
|
6366
|
+
writeback: z.array(z.object({ source: z.string(), mode: z.enum(["sync", "async"]) })).optional(),
|
|
6367
|
+
/** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
|
|
6368
|
+
localOnly: z.array(z.string()).optional(),
|
|
4981
6369
|
alias: z.string(),
|
|
4982
6370
|
table: z.string(),
|
|
4983
6371
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -5062,6 +6450,43 @@ var compiledOperationSchema = z.object({
|
|
|
5062
6450
|
}
|
|
5063
6451
|
}
|
|
5064
6452
|
});
|
|
6453
|
+
var resolvedSourceIrSchema = z.object({
|
|
6454
|
+
name: z.string(),
|
|
6455
|
+
kind: z.enum(["manual", "etl", "prefetch"]),
|
|
6456
|
+
/** Logical table key of the backing relation. */
|
|
6457
|
+
table: z.string(),
|
|
6458
|
+
columns: z.array(z.string()),
|
|
6459
|
+
key: z.string().optional(),
|
|
6460
|
+
fetch: z.union([
|
|
6461
|
+
z.object({ shape: z.literal("single") }),
|
|
6462
|
+
z.object({ shape: z.literal("batch"), maxKeys: z.number().int().positive() })
|
|
6463
|
+
]).optional(),
|
|
6464
|
+
function: z.object({ kind: z.enum(["managed", "native"]), id: z.string() }).optional(),
|
|
6465
|
+
prefetchDefaults: z.object({
|
|
6466
|
+
maxStalenessMs: z.number().int().positive(),
|
|
6467
|
+
timeoutMs: z.number().int().positive(),
|
|
6468
|
+
onError: z.enum(["fallback", "fail"])
|
|
6469
|
+
}).optional(),
|
|
6470
|
+
writeback: z.object({
|
|
6471
|
+
columns: z.array(z.string()),
|
|
6472
|
+
defaultMode: z.enum(["sync", "async"]),
|
|
6473
|
+
timeoutMs: z.number().int().positive(),
|
|
6474
|
+
clearManualOnConfirm: z.boolean()
|
|
6475
|
+
}).optional()
|
|
6476
|
+
});
|
|
6477
|
+
var resolvedMetaIrSchema = z.object({
|
|
6478
|
+
/** Logical pk column of the resolved view (single-column). */
|
|
6479
|
+
pk: z.string(),
|
|
6480
|
+
/** Source name of the manual source. */
|
|
6481
|
+
manual: z.string(),
|
|
6482
|
+
sources: z.array(resolvedSourceIrSchema),
|
|
6483
|
+
deletedBy: z.array(z.string()),
|
|
6484
|
+
/**
|
|
6485
|
+
* Materialized: the view reads `table`, which the data plane refreshes from
|
|
6486
|
+
* the `compute` view (claims-gated) whenever it writes a source row.
|
|
6487
|
+
*/
|
|
6488
|
+
materialized: z.object({ table: z.string(), compute: z.string() }).optional()
|
|
6489
|
+
});
|
|
5065
6490
|
var tableMetaIrSchema = z.object({
|
|
5066
6491
|
table: z.string(),
|
|
5067
6492
|
schema: z.string(),
|
|
@@ -5102,6 +6527,22 @@ var tableMetaIrSchema = z.object({
|
|
|
5102
6527
|
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
5103
6528
|
*/
|
|
5104
6529
|
touchOnUpdate: z.array(z.string()).optional(),
|
|
6530
|
+
/**
|
|
6531
|
+
* Per-cell metadata: logical column → the logical names of its cell-part
|
|
6532
|
+
* columns (which are listed in `columns`, but never filterable, orderable or
|
|
6533
|
+
* writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
|
|
6534
|
+
* write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
|
|
6535
|
+
* the relation tracks no cells.
|
|
6536
|
+
*/
|
|
6537
|
+
cells: z.record(z.string(), z.object({
|
|
6538
|
+
at: z.string().optional(),
|
|
6539
|
+
by: z.string().optional(),
|
|
6540
|
+
rev: z.string().optional(),
|
|
6541
|
+
source: z.string().optional(),
|
|
6542
|
+
conflicts: z.string().optional()
|
|
6543
|
+
}).strict()).optional(),
|
|
6544
|
+
/** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
|
|
6545
|
+
resolved: resolvedMetaIrSchema.optional(),
|
|
5105
6546
|
constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
|
|
5106
6547
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
5107
6548
|
dynamicFilterMaxDepth: z.number().int().positive().default(6),
|
|
@@ -5446,12 +6887,12 @@ async function writeMigration({
|
|
|
5446
6887
|
const tip = workspace.journal.entries.at(-1);
|
|
5447
6888
|
const ordinal = (tip?.ordinal ?? 0) + 1;
|
|
5448
6889
|
const name = migrationName(ordinal, contentHash);
|
|
5449
|
-
const
|
|
6890
|
+
const sql7 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
|
|
5450
6891
|
const metaDir = join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
|
|
5451
6892
|
await mkdir(metaDir, { recursive: true });
|
|
5452
6893
|
const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
|
|
5453
6894
|
`;
|
|
5454
|
-
await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`),
|
|
6895
|
+
await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql7);
|
|
5455
6896
|
await writeFile(join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
|
|
5456
6897
|
const journal = {
|
|
5457
6898
|
...workspace.journal,
|
|
@@ -5462,7 +6903,7 @@ async function writeMigration({
|
|
|
5462
6903
|
name,
|
|
5463
6904
|
content_hash: contentHash,
|
|
5464
6905
|
previous_content_hash: tip?.content_hash ?? null,
|
|
5465
|
-
checksum: checksumOf(
|
|
6906
|
+
checksum: checksumOf(sql7),
|
|
5466
6907
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5467
6908
|
destructive
|
|
5468
6909
|
}
|
|
@@ -5577,8 +7018,8 @@ async function checkWorkspace({
|
|
|
5577
7018
|
let previousId3;
|
|
5578
7019
|
let previousHash = null;
|
|
5579
7020
|
for (const entry of workspace.journal.entries) {
|
|
5580
|
-
const
|
|
5581
|
-
if (checksumOf(
|
|
7021
|
+
const sql7 = await readMigrationSql(dir, entry);
|
|
7022
|
+
if (checksumOf(sql7) !== entry.checksum) {
|
|
5582
7023
|
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
5583
7024
|
return 1;
|
|
5584
7025
|
}
|
|
@@ -5618,7 +7059,7 @@ async function checkWorkspace({
|
|
|
5618
7059
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
5619
7060
|
return 0;
|
|
5620
7061
|
}
|
|
5621
|
-
var firstLine = (
|
|
7062
|
+
var firstLine = (sql7) => sql7.split("\n")[0] ?? sql7;
|
|
5622
7063
|
|
|
5623
7064
|
// src/migrations/from-yaml.ts
|
|
5624
7065
|
function convertV1DefinitionToCanonical(definition) {
|
|
@@ -5724,7 +7165,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5724
7165
|
throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
|
|
5725
7166
|
}
|
|
5726
7167
|
const name = migrationName(ordinal, newHash);
|
|
5727
|
-
const
|
|
7168
|
+
const sql7 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
|
|
5728
7169
|
const snapshotText = `${JSON.stringify(snapshot, null, 2)}
|
|
5729
7170
|
`;
|
|
5730
7171
|
const entry = {
|
|
@@ -5732,11 +7173,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5732
7173
|
name,
|
|
5733
7174
|
content_hash: newHash,
|
|
5734
7175
|
previous_content_hash: previousHash,
|
|
5735
|
-
checksum: checksumOf(
|
|
7176
|
+
checksum: checksumOf(sql7),
|
|
5736
7177
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5737
7178
|
destructive: false
|
|
5738
7179
|
};
|
|
5739
|
-
return { name, entry, sql:
|
|
7180
|
+
return { name, entry, sql: sql7, snapshotText };
|
|
5740
7181
|
}
|
|
5741
7182
|
function renderRebaseMigrationSql({
|
|
5742
7183
|
name,
|
|
@@ -5834,6 +7275,6 @@ function canonicalActions(value) {
|
|
|
5834
7275
|
);
|
|
5835
7276
|
}
|
|
5836
7277
|
|
|
5837
|
-
export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CANONICAL_FORMAT, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CLAIMS_CONTRACT, CLAIMS_SETTING, CLAIM_KEY_PATTERN, CMP_OPS, COLUMN_BRAND, COLUMN_DEFAULT_FNS, COLUMN_IDENTIFIER_PATTERN, COLUMN_TYPES2 as COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, ColumnBuilder, DATE_BUCKETS, DEFINITION_FILE_EXTENSIONS, ENUM_BRAND, HASH_DOMAIN, IDENTIFIER_PATTERN, IDENTIFIER_RE, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LEGACY_IR_VERSION, LINK_BRAND, MAX_DEFINITION_FILE_BYTES, MAX_GENERATED_SDL_BYTES, MAX_IDENTIFIER_LENGTH, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_MIGRATION_EXPORT_FILE, ORM_MIGRATION_EXPORT_MAX_BYTES, ORM_REGISTRATION_MAX_BYTES, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, ORM_TO_PIPELINE_TYPE, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, ROLES_CLAIM_KEY, SCHEMA_DIR, STATEMENT_KINDS, SYNCED_CONTRACT_HASH_DOMAIN, SYNCED_SOURCE_PATTERN, SYNCED_VIEW_EXECUTION_OWNER_ROLE, TABLE_BRAND, VIEW_BRAND, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, activeAuthoringContext, applyMigrationHints, applyMigrationHintsV2, beginAuthoringContext, buildActionParamsSchema, buildDrizzleEntities, buildDrizzleEntitiesV2, buildNamespaceDefinition, buildRebaseJournalEntry, canonicalActionSchema, canonicalColumnSchema, canonicalDefinitionSchema, canonicalEnumSchema, canonicalIndexSchema, canonicalLinkSchema, canonicalPoliciesSchema, canonicalRelations, canonicalTableSchema, canonicalViewSchema, canonicalizeDefinition, capabilityExpression, castParts, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema2 as columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredIrToSql, compilePredicate, compiledArtifactSchema, compiledOperationSchema, computeCanonicalHash, computeDefinitionHash, computeSyncedContractHash, consumerRoleName, convertV1DefinitionToCanonical, createOrmMigrationExport, declaredCapabilityKeys, defineEnum, defineNamespace, defineSyncedTable, defineTable, defineView, deriveColumnName, diffCommand, diffStatements, emptyJournal, emptySnapshot, endAuthoringContext, extractNamedParams, extractOrmSyncedTableContract, generateNamespaceTypes, generateSnapshot, generateSnapshotV2, identifierSchema, indexByHash, isColumnBuilder, isEnumRef, isFunctionDefault, isOwnedStatement, isOwnedStatementV2, isQualifiedReference, isSeqLink, isSeqNamespaceConfig, isSeqTable, isSeqView, limitIrSchema, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, loadNamespaceFromDir, migrationName, namespaceConfigSchema, namespaceSchema, normalizeCanonical, normalizeIr, normalizeSnapshot, orderIrSchema, ormMigrationExportSchema, ormSchemaName, overlayWinsUnion, overrideRowMutation, parseCanonicalDefinition, parseDefinitionSources, parseOrmMigrationExport, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, pendingKitStatementsV2, planMigration, planMigrationV2, policyExpressionSchema, policyVariantSchema, predIrSchema, predicateOperands, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, rebaseViolations, referencedCapabilities, referencedColumns, renderMigrationSql, renderOrmMigrationExport, requiredArtifactIrVersion, revertOverlayMutation, rewriteSyncedSourceRelationsInViewQuery, roleExpression, rootIrSchema, securityStatements, selectionIrSchema, serializeNamespace, sqlTypeName, stableJson, syncedBackingRevokeStatements, syncedContractSurface, syncedTables, syncedViewDropStatements, syncedViewStatements, t, tableFileSchema, tableMetaIrSchema, tableReferenceSchema, toMany, toOne, toPlatformUser, typesInterfaceName, validateViewCatalogStatement, valueIrSchema, variableDefaultIrSchema, variableIrSchema, verifyRebaseIsNoop, walkPredicate, writeMigration };
|
|
7278
|
+
export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CANONICAL_FORMAT, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CELLS_FIELD, CELL_PARTS, CELL_PART_SUFFIX, CLAIMS_CONTRACT, CLAIMS_SETTING, CLAIM_KEY_PATTERN, CMP_OPS, COLUMN_BRAND, COLUMN_DEFAULT_FNS, COLUMN_IDENTIFIER_PATTERN, COLUMN_TYPES2 as COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, ColumnBuilder, DATE_BUCKETS, DEFINITION_FILE_EXTENSIONS, ENUM_BRAND, HASH_DOMAIN, IDENTIFIER_PATTERN, IDENTIFIER_RE, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LEGACY_IR_VERSION, LINK_BRAND, MATERIALIZE_ACTOR, MATERIALIZE_CLAIMS_JSON, MAX_DEFINITION_FILE_BYTES, MAX_GENERATED_SDL_BYTES, MAX_IDENTIFIER_LENGTH, MAX_TRACKED_COLUMN_NAME, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_MIGRATION_EXPORT_FILE, ORM_MIGRATION_EXPORT_MAX_BYTES, ORM_REGISTRATION_MAX_BYTES, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, ORM_TO_PIPELINE_TYPE, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, ROLES_CLAIM_KEY, SCHEMA_DIR, STATEMENT_KINDS, SYNCED_CONTRACT_HASH_DOMAIN, SYNCED_SOURCE_PATTERN, SYNCED_VIEW_EXECUTION_OWNER_ROLE, TABLE_BRAND, VIEW_BRAND, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, activeAuthoringContext, applyMigrationHints, applyMigrationHintsV2, beginAuthoringContext, buildActionParamsSchema, buildDrizzleEntities, buildDrizzleEntitiesV2, buildNamespaceDefinition, buildRebaseJournalEntry, canonicalActionSchema, canonicalColumnSchema, canonicalDefinitionSchema, canonicalEnumSchema, canonicalIndexSchema, canonicalLinkSchema, canonicalPoliciesSchema, canonicalRelations, canonicalTableSchema, canonicalViewSchema, canonicalizeDefinition, capabilityExpression, castParts, cellColumnName, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema2 as columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredIrToSql, compilePredicate, compiledArtifactSchema, compiledOperationSchema, computeCanonicalHash, computeDefinitionHash, computeSyncedContractHash, consumerRoleName, convertV1DefinitionToCanonical, createOrmMigrationExport, custom, declaredCapabilityKeys, defineEnum, defineNamespace, defineResolvedTable, defineSyncedTable, defineTable, defineView, deriveColumnName, diffCommand, diffStatements, emptyJournal, emptySnapshot, endAuthoringContext, etlSource, extractNamedParams, extractOrmSyncedTableContract, generateNamespaceTypes, generateSnapshot, generateSnapshotV2, highest, identifierSchema, indexByHash, isColumnBuilder, isEnumRef, isFunctionDefault, isOwnedStatement, isOwnedStatementV2, isQualifiedReference, isSeqLink, isSeqNamespaceConfig, isSeqTable, isSeqView, latest, limitIrSchema, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, loadNamespaceFromDir, lowest, managedFn, manualSource, materializeLockKey, materializedRefreshFunctionName, migrationName, namespaceConfigSchema, namespaceSchema, nonNullFirst, normalizeCanonical, normalizeIr, normalizeSnapshot, orderIrSchema, ormMigrationExportSchema, ormSchemaName, overlayWinsUnion, overrideRowMutation, parseCanonicalDefinition, parseDefinitionSources, parseOrmMigrationExport, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, pendingKitStatementsV2, planCommittedMigrationV2, planMigration, planMigrationV2, policyExpressionSchema, policyVariantSchema, predIrSchema, predicateOperands, prefetchPlanIrSchema, prefetchSource, priority, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, rebaseViolations, referencedCapabilities, referencedColumns, renderMigrationSql, renderOrmMigrationExport, requiredArtifactIrVersion, resolvedMetaIrSchema, resolvedSourceIrSchema, revertOverlayMutation, rewriteSyncedSourceRelationsInViewQuery, roleExpression, rootIrSchema, securityStatements, selectionIrSchema, serializeNamespace, sqlTypeName, stableJson, syncedBackingRevokeStatements, syncedContractSurface, syncedTables, syncedViewDropStatements, syncedViewStatements, t, tableFileSchema, tableMetaIrSchema, tableReferenceSchema, toMany, toOne, toPlatformUser, typesInterfaceName, validateViewCatalogStatement, valueIrSchema, variableDefaultIrSchema, variableIrSchema, verifyRebaseIsNoop, walkPredicate, writeMigration };
|
|
5838
7279
|
//# sourceMappingURL=index.js.map
|
|
5839
7280
|
//# sourceMappingURL=index.js.map
|