@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.
Files changed (69) hide show
  1. package/README.md +38 -1
  2. package/dist/authoring.cjs +1358 -45
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +223 -2
  5. package/dist/authoring.d.ts +223 -2
  6. package/dist/authoring.js +1349 -47
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +662 -38
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +696 -53
  11. package/dist/canonical.d.ts +696 -53
  12. package/dist/canonical.js +647 -39
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +1808 -247
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +1809 -248
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs +90 -25
  19. package/dist/client.cjs.map +1 -1
  20. package/dist/client.d.cts +39 -22
  21. package/dist/client.d.ts +39 -22
  22. package/dist/client.js +87 -26
  23. package/dist/client.js.map +1 -1
  24. package/dist/compiler.cjs +1325 -285
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +23 -2
  27. package/dist/compiler.d.ts +23 -2
  28. package/dist/compiler.js +1325 -285
  29. package/dist/compiler.js.map +1 -1
  30. package/dist/constants-YoFGjIBM.d.cts +99 -0
  31. package/dist/constants-YoFGjIBM.d.ts +99 -0
  32. package/dist/dynamic-input.d.cts +1 -0
  33. package/dist/dynamic-input.d.ts +1 -0
  34. package/dist/index.cjs +1605 -140
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +230 -113
  37. package/dist/index.d.ts +230 -113
  38. package/dist/index.js +1583 -142
  39. package/dist/index.js.map +1 -1
  40. package/dist/ir.cjs +727 -38
  41. package/dist/ir.cjs.map +1 -1
  42. package/dist/ir.d.cts +539 -21
  43. package/dist/ir.d.ts +539 -21
  44. package/dist/ir.js +722 -39
  45. package/dist/ir.js.map +1 -1
  46. package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
  47. package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
  48. package/dist/policy-1iZbGnZZ.d.cts +24 -0
  49. package/dist/policy-1iZbGnZZ.d.ts +24 -0
  50. package/dist/predicate-v2.cjs +726 -42
  51. package/dist/predicate-v2.cjs.map +1 -1
  52. package/dist/predicate-v2.d.cts +2 -1
  53. package/dist/predicate-v2.d.ts +2 -1
  54. package/dist/predicate-v2.js +726 -42
  55. package/dist/predicate-v2.js.map +1 -1
  56. package/dist/sdl.cjs +858 -54
  57. package/dist/sdl.cjs.map +1 -1
  58. package/dist/sdl.d.cts +7 -3
  59. package/dist/sdl.d.ts +7 -3
  60. package/dist/sdl.js +856 -55
  61. package/dist/sdl.js.map +1 -1
  62. package/package.json +9 -4
  63. package/templates/orm-namespace/CLAUDE.md +6 -4
  64. package/templates/orm-namespace/codegen.preset.d.cts +7 -0
  65. package/templates/orm-namespace/codegen.ts +2 -1
  66. package/templates/orm-namespace/graphql/example/queries.ts +4 -3
  67. package/templates/orm-namespace/package.json +2 -1
  68. package/templates/orm-namespace/schema/tables.ts +3 -2
  69. package/templates/orm-namespace/sequence.config.ts +7 -11
@@ -2,8 +2,8 @@
2
2
 
3
3
  var drizzleOrm = require('drizzle-orm');
4
4
  var pgCore = require('drizzle-orm/pg-core');
5
- var crypto = require('crypto');
6
5
  var zod = require('zod');
6
+ var crypto = require('crypto');
7
7
  var jiti = require('jiti');
8
8
  var promises = require('fs/promises');
9
9
  var module$1 = require('module');
@@ -37,7 +37,8 @@ var ColumnBuilder = class {
37
37
  description: void 0,
38
38
  renamedFrom: void 0,
39
39
  cast: void 0,
40
- deprecated: false
40
+ deprecated: false,
41
+ cell: void 0
41
42
  };
42
43
  }
43
44
  /** Marks this column as (part of) the primary key. Implies NOT NULL. */
@@ -97,6 +98,11 @@ var ColumnBuilder = class {
97
98
  this.state.deprecated = true;
98
99
  return this;
99
100
  }
101
+ /** @internal System cell-metadata part of logical column `of` (generated resolved views only). */
102
+ cellPart(of, part) {
103
+ this.state.cell = { of, part };
104
+ return this;
105
+ }
100
106
  /** Whether `.primaryKey()` was called — collected into the table-level PK. */
101
107
  get isPrimaryKey() {
102
108
  return this.state.primaryKey;
@@ -125,6 +131,7 @@ var ColumnBuilder = class {
125
131
  if (this.state.description !== void 0) column.description = this.state.description;
126
132
  if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
127
133
  if (this.state.cast !== void 0) column.cast = this.state.cast;
134
+ if (this.state.cell !== void 0) column.cell = { ...this.state.cell };
128
135
  return column;
129
136
  }
130
137
  };
@@ -153,6 +160,21 @@ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
153
160
  var MAX_IDENTIFIER_LENGTH = 63;
154
161
  var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
155
162
  var HASH_DOMAIN = "sequence-orm-definitions\0";
163
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
164
+ var CELL_PART_SUFFIX = {
165
+ at: "__at",
166
+ by: "__by",
167
+ rev: "__rev",
168
+ source: "__src",
169
+ conflicts: "__lost"
170
+ };
171
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
172
+ var CELLS_FIELD = "cells";
173
+ function cellColumnName(column, part) {
174
+ return `${column}${CELL_PART_SUFFIX[part]}`;
175
+ }
176
+ var MATERIALIZE_ACTOR = "orm:materialize";
177
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
156
178
 
157
179
  // src/errors.ts
158
180
  var OrmDefinitionError = class extends Error {
@@ -221,6 +243,8 @@ function toOne(target, options) {
221
243
  on: options.on,
222
244
  references: options.references,
223
245
  enforce: options.enforce ?? false,
246
+ onDelete: options.onDelete,
247
+ onUpdate: options.onUpdate,
224
248
  description: options.description
225
249
  };
226
250
  }
@@ -232,6 +256,8 @@ function toMany(target, options) {
232
256
  on: options.on,
233
257
  references: options.references,
234
258
  enforce: false,
259
+ onDelete: void 0,
260
+ onUpdate: void 0,
235
261
  description: options.description
236
262
  };
237
263
  }
@@ -243,6 +269,8 @@ function toPlatformUser(options) {
243
269
  on: options.on,
244
270
  references: options.references,
245
271
  enforce: false,
272
+ onDelete: void 0,
273
+ onUpdate: void 0,
246
274
  description: options.description
247
275
  };
248
276
  }
@@ -277,6 +305,12 @@ function defineTable(name, columns, config = {}) {
277
305
  }
278
306
  const columnName = builder.columnNameFor(propertyName);
279
307
  assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
308
+ if (config.materializes === void 0 && (propertyName === CELLS_FIELD || Object.values(CELL_PART_SUFFIX).some((suffix) => columnName.endsWith(suffix) || propertyName.endsWith(suffix)))) {
309
+ throw new OrmDefinitionError(
310
+ `table "${name}": "${propertyName}" collides with the reserved cell-metadata names ('${CELLS_FIELD}', or a ${Object.values(CELL_PART_SUFFIX).join("/")} suffix)`,
311
+ file
312
+ );
313
+ }
280
314
  const existing = seenColumnNames.get(columnName);
281
315
  if (existing !== void 0) {
282
316
  throw new OrmDefinitionError(
@@ -524,11 +558,11 @@ function compileViewQueries(input) {
524
558
  );
525
559
  }
526
560
  if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
527
- const tableDependencies = pgCore.extractUsedTable(config.query).map((physical) => {
528
- const logical = physicalToLogical.get(physical);
561
+ const tableDependencies = pgCore.extractUsedTable(config.query).map((physical2) => {
562
+ const logical = physicalToLogical.get(physical2);
529
563
  if (!logical) {
530
564
  throw new OrmDefinitionError(
531
- `view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
565
+ `view "${authored.viewName}" query references unmanaged relation "${physical2}" \u2014 use relation(...) with a namespace table or view`,
532
566
  authored.file
533
567
  );
534
568
  }
@@ -852,9 +886,468 @@ function scalarColumnType(table, property, helper) {
852
886
  }
853
887
  return type;
854
888
  }
889
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
890
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
891
+
892
+ // src/checks/compile.ts
893
+ function tokenize(source) {
894
+ const tokens = [];
895
+ for (let position = 0; position < source.length; ) {
896
+ const char = source[position];
897
+ if (/\s/.test(char)) {
898
+ position++;
899
+ continue;
900
+ }
901
+ if (char === "(" || char === ")" || char === ",") {
902
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
903
+ position++;
904
+ continue;
905
+ }
906
+ if (char === "'") {
907
+ const start = position++;
908
+ let value = "";
909
+ let closed = false;
910
+ while (position < source.length) {
911
+ if (source[position] !== "'") {
912
+ value += source[position++];
913
+ continue;
914
+ }
915
+ if (source[position + 1] === "'") {
916
+ value += "'";
917
+ position += 2;
918
+ continue;
919
+ }
920
+ position++;
921
+ closed = true;
922
+ break;
923
+ }
924
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
925
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
926
+ tokens.push({ kind: "string", value, position: start });
927
+ continue;
928
+ }
929
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
930
+ if (operator) {
931
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
932
+ position += operator[0].length;
933
+ continue;
934
+ }
935
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
936
+ if (number) {
937
+ tokens.push({ kind: "number", value: number[0], position });
938
+ position += number[0].length;
939
+ continue;
940
+ }
941
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
942
+ if (identifier3) {
943
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
944
+ position += identifier3[0].length;
945
+ continue;
946
+ }
947
+ throw new Error(`unexpected character '${char}' at position ${position}`);
948
+ }
949
+ tokens.push({ kind: "end", value: "", position: source.length });
950
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
951
+ return tokens;
952
+ }
953
+ var Parser = class {
954
+ constructor(tokens, columns) {
955
+ this.tokens = tokens;
956
+ this.columns = /* @__PURE__ */ new Map();
957
+ for (const column of columns) {
958
+ for (const alias of [column.name, column.columnName]) {
959
+ const previous = this.columns.get(alias);
960
+ if (previous && previous.columnName !== column.columnName) {
961
+ throw new Error(`ambiguous check column '${alias}'`);
962
+ }
963
+ this.columns.set(alias, column);
964
+ }
965
+ }
966
+ }
967
+ index = 0;
968
+ depth = 0;
969
+ columns;
970
+ parse() {
971
+ const expression = this.parseOr();
972
+ this.expect("end", "end of expression");
973
+ return expression;
974
+ }
975
+ parseOr() {
976
+ let left = this.parseAnd();
977
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
978
+ return left;
979
+ }
980
+ parseAnd() {
981
+ let left = this.parseNot();
982
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
983
+ return left;
984
+ }
985
+ parseNot() {
986
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
987
+ return this.parsePrimary();
988
+ }
989
+ parsePrimary() {
990
+ if (this.peek().kind === "lparen") {
991
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
992
+ this.advance();
993
+ const expression = this.parseOr();
994
+ this.expect("rparen", "')'");
995
+ this.depth--;
996
+ return `(${expression})`;
997
+ }
998
+ return this.parseComparison();
999
+ }
1000
+ parseComparison() {
1001
+ const left = this.parseValue();
1002
+ if (this.matchKeyword("is")) {
1003
+ const negate = this.matchKeyword("not");
1004
+ this.expectKeyword("null");
1005
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
1006
+ }
1007
+ if (this.matchKeyword("between")) {
1008
+ const lower = this.parseValue();
1009
+ this.expectKeyword("and");
1010
+ const upper = this.parseValue();
1011
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
1012
+ }
1013
+ if (this.matchKeyword("in")) {
1014
+ this.expect("lparen", "'(' after IN");
1015
+ const values = [this.parseValue()];
1016
+ while (this.peek().kind === "comma") {
1017
+ this.advance();
1018
+ values.push(this.parseValue());
1019
+ }
1020
+ this.expect("rparen", "')' after IN values");
1021
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
1022
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
1023
+ }
1024
+ const operator = this.expect("operator", "a comparison operator");
1025
+ const right = this.parseValue();
1026
+ if (operator.value === "~" || operator.value === "!~") {
1027
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
1028
+ if (!right.literal) throw new Error("regex pattern must be a literal");
1029
+ }
1030
+ return `${left.sql} ${operator.value} ${right.sql}`;
1031
+ }
1032
+ parseValue() {
1033
+ const token = this.advance();
1034
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
1035
+ if (token.kind === "number") {
1036
+ const number = Number(token.value);
1037
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
1038
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
1039
+ }
1040
+ return { sql: token.value, literal: true };
1041
+ }
1042
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
1043
+ const keyword = token.value.toLowerCase();
1044
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
1045
+ if (keyword === "btrim") {
1046
+ this.expect("lparen", "'(' after btrim");
1047
+ const column2 = this.parseColumn();
1048
+ this.expect("rparen", "')' after btrim column");
1049
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
1050
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
1051
+ }
1052
+ const column = this.columns.get(token.value);
1053
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
1054
+ return { sql: quoteIdentifier(column.columnName), column };
1055
+ }
1056
+ parseColumn() {
1057
+ const token = this.expect("identifier", "a column name");
1058
+ const column = this.columns.get(token.value);
1059
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
1060
+ return column;
1061
+ }
1062
+ peek() {
1063
+ return this.tokens[this.index];
1064
+ }
1065
+ advance() {
1066
+ const token = this.peek();
1067
+ if (token.kind !== "end") this.index++;
1068
+ return token;
1069
+ }
1070
+ matchKeyword(keyword) {
1071
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
1072
+ this.advance();
1073
+ return true;
1074
+ }
1075
+ expectKeyword(keyword) {
1076
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
1077
+ }
1078
+ expect(kind, description) {
1079
+ const token = this.peek();
1080
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
1081
+ return this.advance();
1082
+ }
1083
+ };
1084
+ function compileCheckExpression(source, columns) {
1085
+ return new Parser(tokenize(source), columns).parse();
1086
+ }
1087
+ var policyVariantSchema = zod.z.object({
1088
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
1089
+ when: zod.z.string().min(1).max(1e3),
1090
+ /** Exactly one of: allow-all bypass, or a row predicate. */
1091
+ allow: zod.z.literal(true).optional(),
1092
+ policy: zod.z.string().min(1).max(2e3).optional()
1093
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
1094
+ message: "A variant declares exactly one of allow: true or policy"
1095
+ });
1096
+ var policyExpressionSchema = zod.z.union([
1097
+ zod.z.string().min(1).max(2e3),
1098
+ zod.z.object({
1099
+ default: zod.z.string().min(1).max(2e3),
1100
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
1101
+ })
1102
+ ]);
1103
+ function parseDurationMs(input) {
1104
+ if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
1105
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
1106
+ if (!match) return void 0;
1107
+ const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
1108
+ const value = Math.round(Number(match[1]) * factor);
1109
+ return value > 0 ? value : void 0;
1110
+ }
1111
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
1112
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
1113
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
1114
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
1115
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
1116
+ };
1117
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
1118
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
1119
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
1120
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
1121
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
1122
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
1123
+ };
1124
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
1125
+ var RESERVED_RESOLVED_COLUMN_NAMES = /* @__PURE__ */ new Set([
1126
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
1127
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
1128
+ ]);
1129
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
1130
+ var canonicalFunctionRefSchema = zod.z.object({
1131
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
1132
+ kind: zod.z.enum(["managed", "native"]),
1133
+ id: zod.z.string().min(1).max(200)
1134
+ }).strict();
1135
+ var canonicalFetchShapeSchema = zod.z.union([
1136
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
1137
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
1138
+ ]);
1139
+ var canonicalResolvedSourceSchema = zod.z.object({
1140
+ name: identifier,
1141
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
1142
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
1143
+ table: identifier,
1144
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
1145
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
1146
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
1147
+ updatedAt: zod.z.string().min(1).max(63).optional(),
1148
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
1149
+ key: zod.z.string().min(1).max(63).optional(),
1150
+ fetch: canonicalFetchShapeSchema.optional(),
1151
+ function: canonicalFunctionRefSchema.optional(),
1152
+ prefetchDefaults: zod.z.object({
1153
+ maxStalenessMs: durationMs,
1154
+ timeoutMs: durationMs,
1155
+ onError: zod.z.enum(["fallback", "fail"])
1156
+ }).strict().optional(),
1157
+ writeback: zod.z.object({
1158
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
1159
+ defaultMode: zod.z.enum(["sync", "async"]),
1160
+ timeoutMs: durationMs,
1161
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
1162
+ clearManualOnConfirm: zod.z.boolean()
1163
+ }).strict().optional()
1164
+ }).strict();
1165
+ var canonicalResolveRuleSchema = zod.z.object({
1166
+ column: zod.z.string().min(1).max(63),
1167
+ comparator: zod.z.enum(COMPARATORS),
1168
+ /** Source names, in tie-break order. */
1169
+ candidates: zod.z.array(identifier).min(1).max(16),
1170
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
1171
+ orderBy: zod.z.string().min(1).max(2e3).optional()
1172
+ }).strict();
1173
+ var canonicalResolvedSchema = zod.z.object({
1174
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
1175
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
1176
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
1177
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
1178
+ deletedBy: zod.z.array(identifier).max(1).default([]),
1179
+ /**
1180
+ * Materialized: the view reads an indexed internal table (`table`) that the
1181
+ * data plane keeps in step with `compute`, the internal view holding the
1182
+ * resolution query. Omitted when the view computes on read.
1183
+ */
1184
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
1185
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
1186
+ policies: zod.z.object({
1187
+ write: policyExpressionSchema.optional(),
1188
+ insert: policyExpressionSchema.optional(),
1189
+ update: policyExpressionSchema.optional()
1190
+ }).strict().default({})
1191
+ }).strict();
1192
+ function validateResolvedView({ view, tables, views = [] }) {
1193
+ const resolved = view.resolved;
1194
+ if (!resolved) return [];
1195
+ const errors = [];
1196
+ const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
1197
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
1198
+ if (!pkColumn) {
1199
+ fail("requires exactly one primary-key column");
1200
+ return errors;
1201
+ }
1202
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
1203
+ const scope = {
1204
+ view,
1205
+ resolved,
1206
+ pkColumn,
1207
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
1208
+ sourceByName: /* @__PURE__ */ new Map(),
1209
+ fail
1210
+ };
1211
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
1212
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
1213
+ for (const source of resolved.sources) {
1214
+ if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
1215
+ scope.sourceByName.set(source.name, source);
1216
+ const table = tableByName.get(source.table);
1217
+ if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
1218
+ else validateSource({ scope, source, table });
1219
+ }
1220
+ validateRules(scope);
1221
+ validateViewCells(scope);
1222
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
1223
+ for (const name of resolved.deletedBy) {
1224
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
1225
+ }
1226
+ return errors;
1227
+ }
1228
+ function validateSource({ scope, source, table }) {
1229
+ const { fail } = scope;
1230
+ if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
1231
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
1232
+ fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
1233
+ }
1234
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
1235
+ if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
1236
+ if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
1237
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
1238
+ validateSourceColumns({ scope, source, byName });
1239
+ if (source.kind === "etl") {
1240
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
1241
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
1242
+ fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
1243
+ }
1244
+ }
1245
+ validatePrefetchFields({ scope, source });
1246
+ }
1247
+ function validatePrefetchFields({ scope, source }) {
1248
+ const { fail, pkColumn } = scope;
1249
+ if (source.kind !== "prefetch") {
1250
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
1251
+ fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
1252
+ }
1253
+ return;
1254
+ }
1255
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
1256
+ fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
1257
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
1258
+ fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
1259
+ }
1260
+ for (const column of source.writeback?.columns ?? []) {
1261
+ if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
1262
+ }
1263
+ }
1264
+ function validateSourceColumns({ scope, source, byName }) {
1265
+ const { fail, pkColumn, dataColumns } = scope;
1266
+ for (const column of [pkColumn.name, ...source.columns]) {
1267
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
1268
+ const theirs = byName.get(column);
1269
+ if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
1270
+ else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
1271
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
1272
+ }
1273
+ }
1274
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
1275
+ const ruled = /* @__PURE__ */ new Set();
1276
+ for (const rule of resolved.resolve) {
1277
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
1278
+ fail(`resolve rule targets unknown column '${rule.column}'`);
1279
+ continue;
1280
+ }
1281
+ if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
1282
+ ruled.add(rule.column);
1283
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
1284
+ fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
1285
+ }
1286
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
1287
+ for (const candidate of rule.candidates) {
1288
+ const source = sourceByName.get(candidate);
1289
+ if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
1290
+ else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
1291
+ }
1292
+ }
1293
+ for (const column of dataColumns.keys()) {
1294
+ if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
1295
+ }
1296
+ }
1297
+ var VIEW_CELL_TYPES = {
1298
+ source: "text",
1299
+ at: "timestamptz",
1300
+ by: "text",
1301
+ conflicts: "jsonb"
1302
+ };
1303
+ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
1304
+ const expected = /* @__PURE__ */ new Map();
1305
+ for (const column of dataColumns.values()) {
1306
+ if (column.name === pkColumn.name) continue;
1307
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
1308
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
1309
+ }
1310
+ }
1311
+ for (const column of view.columns) {
1312
+ if (!column.cell) continue;
1313
+ const want = expected.get(column.name);
1314
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
1315
+ fail(`unexpected cell column '${column.name}'`);
1316
+ continue;
1317
+ }
1318
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
1319
+ expected.delete(column.name);
1320
+ }
1321
+ for (const name of expected.keys()) fail(`missing cell column '${name}'`);
1322
+ }
1323
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
1324
+ function validateMaterialized({ scope, tableByName, views }) {
1325
+ const { view, resolved, fail } = scope;
1326
+ const { table: tableName, compute: computeName } = resolved.materialized;
1327
+ const table = tableByName.get(tableName);
1328
+ if (!table || table.internal !== true || table.materializes !== view.view) {
1329
+ fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
1330
+ } else {
1331
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
1332
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
1333
+ }
1334
+ const compute = views.find((candidate) => candidate.view === computeName);
1335
+ if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
1336
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
1337
+ for (const source of resolved.sources) {
1338
+ if (source.kind !== "etl") continue;
1339
+ const read = tableByName.get(source.table)?.policies?.read;
1340
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
1341
+ if (!unconditional) {
1342
+ fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ // src/canonical/v2.ts
855
1348
  var CANONICAL_FORMAT = 2;
856
1349
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
857
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
1350
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
858
1351
  var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
859
1352
  var COLUMN_TYPES = [
860
1353
  "uuid",
@@ -870,14 +1363,14 @@ var COLUMN_TYPES = [
870
1363
  ];
871
1364
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
872
1365
  var canonicalEnumSchema = zod.z.object({
873
- name: identifier,
1366
+ name: identifier2,
874
1367
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
875
1368
  });
876
1369
  var canonicalColumnSchema = zod.z.object({
877
1370
  /** Property name in TS (camelCase); columnName is the Postgres name. */
878
1371
  name: zod.z.string().min(1).max(63),
879
1372
  columnName: columnIdentifier,
880
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
1373
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
881
1374
  nullable: zod.z.boolean().default(true),
882
1375
  unique: zod.z.boolean().default(false),
883
1376
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -893,7 +1386,14 @@ var canonicalColumnSchema = zod.z.object({
893
1386
  /** Migration hints — same semantics as v1. */
894
1387
  renamedFrom: columnIdentifier.optional(),
895
1388
  cast: zod.z.string().max(200).optional(),
896
- deprecated: zod.z.boolean().default(false)
1389
+ deprecated: zod.z.boolean().default(false),
1390
+ /**
1391
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
1392
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
1393
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
1394
+ * ordinary columns so their hashes are unchanged.
1395
+ */
1396
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
897
1397
  }).superRefine((column, ctx) => {
898
1398
  if (column.onUpdateFn === void 0) return;
899
1399
  if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
@@ -908,36 +1408,26 @@ var canonicalLinkSchema = zod.z.object({
908
1408
  name: zod.z.string().min(1).max(63),
909
1409
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
910
1410
  /** Target table name (namespace-local). Absent for toPlatformUser. */
911
- target: identifier.optional(),
1411
+ target: identifier2.optional(),
912
1412
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
913
1413
  on: columnIdentifier,
914
1414
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
915
1415
  references: columnIdentifier.optional(),
916
1416
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
917
1417
  enforce: zod.z.boolean().default(false),
1418
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
1419
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
918
1420
  description: zod.z.string().max(500).optional()
919
1421
  });
920
1422
  var canonicalIndexSchema = zod.z.object({
921
1423
  columns: zod.z.array(columnIdentifier).min(1),
922
1424
  unique: zod.z.boolean().default(false),
923
- name: identifier.optional()
1425
+ name: identifier2.optional()
924
1426
  });
925
- var policyVariantSchema = zod.z.object({
926
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
927
- when: zod.z.string().min(1).max(1e3),
928
- /** Exactly one of: allow-all bypass, or a row predicate. */
929
- allow: zod.z.literal(true).optional(),
930
- policy: zod.z.string().min(1).max(2e3).optional()
931
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
932
- message: "A variant declares exactly one of allow: true or policy"
933
- });
934
- var policyExpressionSchema = zod.z.union([
935
- zod.z.string().min(1).max(2e3),
936
- zod.z.object({
937
- default: zod.z.string().min(1).max(2e3),
938
- variants: zod.z.array(policyVariantSchema).max(10).default([])
939
- })
940
- ]);
1427
+ var canonicalCheckSchema = zod.z.object({
1428
+ name: identifier2,
1429
+ expression: zod.z.string().min(1).max(2e3)
1430
+ }).strict();
941
1431
  var canonicalPoliciesSchema = zod.z.object({
942
1432
  read: policyExpressionSchema.optional(),
943
1433
  write: policyExpressionSchema.optional(),
@@ -967,8 +1457,165 @@ function isSingleViewQuerySql(value) {
967
1457
  }
968
1458
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
969
1459
  }
1460
+ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
1461
+ "abs",
1462
+ "age",
1463
+ "array_agg",
1464
+ "array_append",
1465
+ "array_cat",
1466
+ "array_dims",
1467
+ "array_length",
1468
+ "array_lower",
1469
+ "array_ndims",
1470
+ "array_position",
1471
+ "array_positions",
1472
+ "array_prepend",
1473
+ "array_remove",
1474
+ "array_replace",
1475
+ "array_to_string",
1476
+ "array_trim",
1477
+ "array_upper",
1478
+ "ascii",
1479
+ "avg",
1480
+ "bit_and",
1481
+ "bit_or",
1482
+ "bit_xor",
1483
+ "bool_and",
1484
+ "bool_or",
1485
+ "btrim",
1486
+ "cardinality",
1487
+ "cbrt",
1488
+ "ceil",
1489
+ "ceiling",
1490
+ "char_length",
1491
+ "character_length",
1492
+ "chr",
1493
+ "concat",
1494
+ "concat_ws",
1495
+ "count",
1496
+ "cume_dist",
1497
+ "date_bin",
1498
+ "date_part",
1499
+ "date_trunc",
1500
+ "decode",
1501
+ "degrees",
1502
+ "dense_rank",
1503
+ "encode",
1504
+ "enum_first",
1505
+ "enum_last",
1506
+ "enum_range",
1507
+ "every",
1508
+ "exp",
1509
+ "first_value",
1510
+ "floor",
1511
+ "format",
1512
+ "grouping",
1513
+ "initcap",
1514
+ "json_agg",
1515
+ "json_array_length",
1516
+ "json_build_array",
1517
+ "json_build_object",
1518
+ "json_extract_path",
1519
+ "json_extract_path_text",
1520
+ "json_object_agg",
1521
+ "json_object_keys",
1522
+ "json_strip_nulls",
1523
+ "json_typeof",
1524
+ "jsonb_agg",
1525
+ "jsonb_array_length",
1526
+ "jsonb_build_array",
1527
+ "jsonb_build_object",
1528
+ "jsonb_extract_path",
1529
+ "jsonb_extract_path_text",
1530
+ "jsonb_insert",
1531
+ "jsonb_object_agg",
1532
+ "jsonb_object_keys",
1533
+ "jsonb_path_exists",
1534
+ "jsonb_path_match",
1535
+ "jsonb_path_query",
1536
+ "jsonb_path_query_array",
1537
+ "jsonb_path_query_first",
1538
+ "jsonb_pretty",
1539
+ "jsonb_set",
1540
+ "jsonb_set_lax",
1541
+ "jsonb_strip_nulls",
1542
+ "jsonb_typeof",
1543
+ "justify_days",
1544
+ "justify_hours",
1545
+ "justify_interval",
1546
+ "lag",
1547
+ "last_value",
1548
+ "lead",
1549
+ "left",
1550
+ "length",
1551
+ "ln",
1552
+ "log",
1553
+ "lower",
1554
+ "lpad",
1555
+ "ltrim",
1556
+ "make_date",
1557
+ "make_interval",
1558
+ "make_time",
1559
+ "make_timestamp",
1560
+ "make_timestamptz",
1561
+ "max",
1562
+ "md5",
1563
+ "min",
1564
+ "mod",
1565
+ "nth_value",
1566
+ "ntile",
1567
+ "octet_length",
1568
+ "percent_rank",
1569
+ "pi",
1570
+ "power",
1571
+ "quote_ident",
1572
+ "quote_literal",
1573
+ "quote_nullable",
1574
+ "radians",
1575
+ "rank",
1576
+ "regexp_count",
1577
+ "regexp_instr",
1578
+ "regexp_like",
1579
+ "regexp_match",
1580
+ "regexp_matches",
1581
+ "regexp_replace",
1582
+ "regexp_split_to_array",
1583
+ "regexp_split_to_table",
1584
+ "repeat",
1585
+ "replace",
1586
+ "reverse",
1587
+ "right",
1588
+ "round",
1589
+ "row_number",
1590
+ "rpad",
1591
+ "rtrim",
1592
+ "sign",
1593
+ "split_part",
1594
+ "sqrt",
1595
+ "starts_with",
1596
+ "string_agg",
1597
+ "string_to_array",
1598
+ "strpos",
1599
+ "substr",
1600
+ "substring",
1601
+ "sum",
1602
+ "timezone",
1603
+ "to_ascii",
1604
+ "to_char",
1605
+ "to_date",
1606
+ "to_hex",
1607
+ "to_json",
1608
+ "to_jsonb",
1609
+ "to_number",
1610
+ "to_timestamp",
1611
+ "translate",
1612
+ "trunc",
1613
+ "unnest",
1614
+ "upper",
1615
+ "width_bucket"
1616
+ ];
970
1617
  var canonicalActionSchema = zod.z.object({
971
- name: identifier,
1618
+ name: identifier2,
972
1619
  requires: zod.z.string().max(1e3).optional(),
973
1620
  params: zod.z.record(zod.z.string(), zod.z.object({
974
1621
  type: columnTypeSchema,
@@ -980,9 +1627,9 @@ var canonicalActionSchema = zod.z.object({
980
1627
  });
981
1628
  var canonicalCowMutationSchema = zod.z.object({
982
1629
  kind: zod.z.enum(["override", "revert"]),
983
- name: identifier,
1630
+ name: identifier2,
984
1631
  /** Synced table (same namespace) the overlay copies from / falls back to. */
985
- synced: identifier,
1632
+ synced: identifier2,
986
1633
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
987
1634
  pk: zod.z.string().min(1).max(63),
988
1635
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -1010,23 +1657,27 @@ var canonicalCowMutationSchema = zod.z.object({
1010
1657
  }
1011
1658
  });
1012
1659
  var canonicalForeignKeySchema = zod.z.object({
1013
- name: identifier,
1660
+ name: identifier2,
1014
1661
  columns: zod.z.array(columnIdentifier).min(1).max(8),
1015
- target: identifier,
1662
+ target: identifier2,
1016
1663
  references: zod.z.array(columnIdentifier).min(1).max(8),
1017
- deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
1664
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
1665
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
1666
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
1018
1667
  }).strict();
1019
1668
  var canonicalTableSchema = zod.z.object({
1020
- table: identifier,
1669
+ table: identifier2,
1021
1670
  description: zod.z.string().max(2e3).optional(),
1022
1671
  /** Table-level rename hint (new in v2). */
1023
- renamedFrom: identifier.optional(),
1672
+ renamedFrom: identifier2.optional(),
1024
1673
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
1025
1674
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
1026
1675
  columns: zod.z.array(canonicalColumnSchema).default([]),
1027
1676
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
1028
1677
  links: zod.z.array(canonicalLinkSchema).default([]),
1029
1678
  indexes: zod.z.array(canonicalIndexSchema).default([]),
1679
+ /** Omitted by old definitions, preserving their existing content hashes. */
1680
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
1030
1681
  policies: canonicalPoliciesSchema.default({}),
1031
1682
  /**
1032
1683
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -1037,22 +1688,55 @@ var canonicalTableSchema = zod.z.object({
1037
1688
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
1038
1689
  cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
1039
1690
  /** Omitted by old definitions, preserving their existing content hashes. */
1040
- foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
1691
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
1692
+ /**
1693
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
1694
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
1695
+ * on a resolved table's internal sources), listed explicitly in `columns`
1696
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
1697
+ * Omitted when off so existing definitions hash unchanged.
1698
+ */
1699
+ cellMetadata: zod.z.literal(true).optional(),
1700
+ /**
1701
+ * System-owned backing table of a resolved table (its manual or prefetch
1702
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
1703
+ * only the data plane writes them. Omitted when false.
1704
+ */
1705
+ internal: zod.z.literal(true).optional(),
1706
+ /**
1707
+ * The materialized copy of resolved table `materializes` (internal): the
1708
+ * same columns as that view, including its cell parts, maintained by the
1709
+ * data plane. Omitted otherwise.
1710
+ */
1711
+ materializes: identifier2.optional()
1041
1712
  });
1713
+ var MAX_VIEW_QUERY = 1e5;
1714
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
1042
1715
  var canonicalViewSchema = zod.z.object({
1043
- view: identifier,
1716
+ view: identifier2,
1044
1717
  description: zod.z.string().max(2e3).optional(),
1045
1718
  columns: zod.z.array(canonicalColumnSchema).min(1),
1046
1719
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
1047
1720
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
1048
1721
  links: zod.z.array(canonicalLinkSchema).default([]),
1049
1722
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
1050
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
1723
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
1051
1724
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
1052
1725
  }),
1053
1726
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
1054
- dependencies: zod.z.array(identifier).max(500)
1727
+ dependencies: zod.z.array(identifier2).max(500),
1728
+ /**
1729
+ * Present when this view is a resolved table (defineResolvedTable): the
1730
+ * multi-source metadata behind its generated query. Omitted otherwise so
1731
+ * existing views hash unchanged.
1732
+ */
1733
+ resolved: canonicalResolvedSchema.optional(),
1734
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
1735
+ internal: zod.z.literal(true).optional()
1055
1736
  }).superRefine((view, ctx) => {
1737
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
1738
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
1739
+ }
1056
1740
  for (const [index, column] of view.columns.entries()) {
1057
1741
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1058
1742
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
@@ -1067,15 +1751,82 @@ var canonicalViewSchema = zod.z.object({
1067
1751
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
1068
1752
  }
1069
1753
  }
1754
+ if (view.resolved === void 0 && view.internal !== true) {
1755
+ for (const [index, column] of view.columns.entries()) {
1756
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
1757
+ }
1758
+ }
1070
1759
  for (const [index, link] of view.links.entries()) {
1071
1760
  if (link.enforce) {
1072
1761
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
1073
1762
  }
1763
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
1764
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
1765
+ }
1074
1766
  }
1075
1767
  });
1768
+ var CELL_PART_TYPES = {
1769
+ at: "timestamptz",
1770
+ by: "text",
1771
+ rev: "uuid",
1772
+ source: "text",
1773
+ conflicts: "jsonb"
1774
+ };
1775
+ function trackedCellColumns(table) {
1776
+ const pk = new Set(table.primaryKey ?? []);
1777
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
1778
+ }
1779
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
1780
+ function validateCellMetadata(table, index, ctx) {
1781
+ const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
1782
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
1783
+ if (table.materializes !== void 0) return;
1784
+ if (table.cellMetadata !== true) {
1785
+ for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
1786
+ return;
1787
+ }
1788
+ if (table.source !== void 0) {
1789
+ fail(`synced table '${table.table}' cannot track cell metadata`);
1790
+ return;
1791
+ }
1792
+ const tracked = trackedCellColumns(table);
1793
+ const byName = new Map(tracked.map((column) => [column.name, column]));
1794
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
1795
+ if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
1796
+ const seen = /* @__PURE__ */ new Set();
1797
+ for (const { column, position, cell } of cells) {
1798
+ const path = ["tables", index, "columns", position];
1799
+ const owner = byName.get(cell.of);
1800
+ if (!owner) {
1801
+ fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
1802
+ continue;
1803
+ }
1804
+ 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);
1805
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
1806
+ fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
1807
+ }
1808
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
1809
+ fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
1810
+ }
1811
+ const key = `${cell.of}:${cell.part}`;
1812
+ if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
1813
+ seen.add(key);
1814
+ }
1815
+ for (const column of tracked) {
1816
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
1817
+ fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
1818
+ }
1819
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
1820
+ fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
1821
+ }
1822
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
1823
+ if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
1824
+ }
1825
+ }
1826
+ }
1076
1827
  var canonicalDefinitionSchema = zod.z.object({
1077
1828
  definition_format: zod.z.literal(CANONICAL_FORMAT),
1078
- namespace: identifier.max(41),
1829
+ namespace: identifier2.max(41),
1079
1830
  description: zod.z.string().max(2e3).optional(),
1080
1831
  enums: zod.z.array(canonicalEnumSchema).default([]),
1081
1832
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
@@ -1093,6 +1844,7 @@ var canonicalDefinitionSchema = zod.z.object({
1093
1844
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
1094
1845
  }
1095
1846
  seen.add(table.table);
1847
+ validateCellMetadata(table, index, ctx);
1096
1848
  if (table.source !== void 0) {
1097
1849
  for (const [columnIndex, column] of table.columns.entries()) {
1098
1850
  if (column.onUpdateFn !== void 0) {
@@ -1112,6 +1864,26 @@ var canonicalDefinitionSchema = zod.z.object({
1112
1864
  ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
1113
1865
  ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
1114
1866
  ]);
1867
+ for (const [index, link] of table.links.entries()) {
1868
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
1869
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
1870
+ }
1871
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
1872
+ const source = table.columns.find((column) => column.columnName === link.on);
1873
+ if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "SET NULL requires a nullable source column" });
1874
+ }
1875
+ }
1876
+ for (const [index, check] of (table.checks ?? []).entries()) {
1877
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
1878
+ if (table.source) fail("synced tables cannot declare checks");
1879
+ if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
1880
+ names.add(check.name);
1881
+ try {
1882
+ compileCheckExpression(check.expression, table.columns);
1883
+ } catch (error) {
1884
+ fail(error instanceof Error ? error.message : String(error));
1885
+ }
1886
+ }
1115
1887
  for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
1116
1888
  const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
1117
1889
  if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
@@ -1129,10 +1901,18 @@ var canonicalDefinitionSchema = zod.z.object({
1129
1901
  if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
1130
1902
  else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
1131
1903
  }
1904
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
1905
+ 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");
1906
+ }
1132
1907
  const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
1133
1908
  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");
1134
1909
  }
1135
1910
  }
1911
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
1912
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
1913
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
1914
+ }
1915
+ }
1136
1916
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
1137
1917
  for (const [tableIndex, table] of definition.tables.entries()) {
1138
1918
  const cowMutations = table.cowMutations ?? [];
@@ -1254,13 +2034,14 @@ function normalizeCanonical(definition) {
1254
2034
  ...definition,
1255
2035
  enums: sortByName(definition.enums),
1256
2036
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
1257
- const { cowMutations, foreignKeys, ...rest } = t2;
2037
+ const { cowMutations, foreignKeys, checks, ...rest } = t2;
1258
2038
  return {
1259
2039
  ...rest,
1260
2040
  links: sortByName(t2.links),
1261
2041
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
1262
2042
  actions: sortByName(t2.actions),
1263
2043
  ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
2044
+ ...checks?.length ? { checks: sortByName(checks) } : {},
1264
2045
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
1265
2046
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
1266
2047
  };
@@ -1275,6 +2056,454 @@ function normalizeCanonical(definition) {
1275
2056
  }
1276
2057
  return normalized;
1277
2058
  }
2059
+
2060
+ // src/authoring/resolved-sources.ts
2061
+ function cloneColumn({ builder, property, notNull, primaryKey }) {
2062
+ const canonical = builder.toCanonicalColumn(property);
2063
+ let clone = new ColumnBuilder(canonical.type, canonical.columnName);
2064
+ if (primaryKey) clone = clone.primaryKey();
2065
+ if (notNull) clone = clone.notNull();
2066
+ if (canonical.description !== void 0) clone = clone.description(canonical.description);
2067
+ return clone;
2068
+ }
2069
+ function planSource(input) {
2070
+ const { sourceName, input: source, fail } = input;
2071
+ if (!IDENTIFIER_PATTERN.test(sourceName)) fail(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
2072
+ switch (source.kind) {
2073
+ case "manual":
2074
+ return {
2075
+ name: sourceName,
2076
+ kind: "manual",
2077
+ columns: input.dataColumns,
2078
+ table: internalTable(input, input.dataColumns, {
2079
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned(),
2080
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName),
2081
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName),
2082
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.columnName)
2083
+ }, { revisions: true })
2084
+ };
2085
+ case "etl":
2086
+ return planEtl(input, source);
2087
+ case "prefetch":
2088
+ return planPrefetch(input, source);
2089
+ }
2090
+ }
2091
+ var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
2092
+ function internalTable(input, supplied, system, options = {}) {
2093
+ const { viewName, sourceName, columns, pk, pkBuilder } = input;
2094
+ const tableColumns = { [pk]: cloneColumn({ builder: pkBuilder, property: pk, notNull: true, primaryKey: true }) };
2095
+ for (const property of supplied) {
2096
+ tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
2097
+ }
2098
+ return defineTable(`${viewName}__${sourceName}`, { ...tableColumns, ...system }, {
2099
+ description: `Internal ${input.input.kind} source "${sourceName}" of resolved table "${viewName}". System-maintained.`,
2100
+ cellMetadata: true,
2101
+ // The manual source identifies each edit (`c__rev`) so an async
2102
+ // confirmation clears only the exact edit it delivered.
2103
+ ...options.revisions ? { cellRevisions: true } : {},
2104
+ internal: true
2105
+ });
2106
+ }
2107
+ function assertColumns(input, list, what) {
2108
+ for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
2109
+ }
2110
+ function planEtl(input, source) {
2111
+ const { sourceName, pk, dataColumns, fail } = input;
2112
+ const table = typeof source.table === "function" ? source.table() : source.table;
2113
+ if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
2114
+ const synced = table;
2115
+ if (!(pk in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
2116
+ const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
2117
+ assertColumns(input, supplied, `etl source "${sourceName}"`);
2118
+ if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
2119
+ fail(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
2120
+ }
2121
+ return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
2122
+ }
2123
+ function planPrefetch(input, source) {
2124
+ const { sourceName, pk, fail } = input;
2125
+ assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
2126
+ if (source.key !== pk && !source.columns.includes(source.key)) {
2127
+ fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
2128
+ }
2129
+ for (const column of source.writeback?.columns ?? []) {
2130
+ if (!source.columns.includes(column)) fail(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
2131
+ }
2132
+ const writeback = source.writeback;
2133
+ return {
2134
+ name: sourceName,
2135
+ kind: "prefetch",
2136
+ columns: [...source.columns],
2137
+ key: source.key,
2138
+ function: source.function,
2139
+ fetch: source.fetch === "single" ? { shape: "single" } : { shape: "batch", maxKeys: source.fetch.batch.maxKeys },
2140
+ prefetchDefaults: {
2141
+ maxStalenessMs: parseDuration(source.prefetchDefaults?.maxStaleness ?? "30s"),
2142
+ timeoutMs: parseDuration(source.prefetchDefaults?.timeout ?? "1500ms"),
2143
+ onError: source.prefetchDefaults?.onError ?? "fallback"
2144
+ },
2145
+ ...writeback ? {
2146
+ writeback: {
2147
+ columns: [...writeback.columns],
2148
+ defaultMode: writeback.defaultMode ?? "sync",
2149
+ timeoutMs: parseDuration(writeback.timeout ?? "5s"),
2150
+ clearManualOnConfirm: writeback.clearManualOnConfirm ?? true
2151
+ }
2152
+ } : {},
2153
+ table: internalTable(input, source.columns, {
2154
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.columnName).notNull().default("now()"),
2155
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned()
2156
+ })
2157
+ };
2158
+ }
2159
+ function parseDuration(input) {
2160
+ const value = parseDurationMs(input);
2161
+ if (value === void 0) throw new OrmDefinitionError(`invalid duration "${input}" \u2014 use e.g. '1500ms', '30s', '5m'`);
2162
+ return value;
2163
+ }
2164
+ var PG_TYPE = {
2165
+ uuid: "uuid",
2166
+ text: "text",
2167
+ integer: "integer",
2168
+ bigint: "bigint",
2169
+ numeric: "numeric",
2170
+ double: "double precision",
2171
+ boolean: "boolean",
2172
+ timestamptz: "timestamptz",
2173
+ date: "date",
2174
+ jsonb: "jsonb"
2175
+ };
2176
+ var ident = (value) => `"${value.replace(/"/g, '""')}"`;
2177
+ var literal = (value) => `'${value.replace(/'/g, "''")}'`;
2178
+ var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
2179
+ function candidateSql({ source, column, pk, pgType }) {
2180
+ const a = ident(`s_${source.name}`);
2181
+ const name = physical({ source, property: column });
2182
+ const value = `CAST(${a}.${ident(name)} AS ${pgType})`;
2183
+ if (source.kind === "etl") {
2184
+ return {
2185
+ source: source.name,
2186
+ value,
2187
+ has: `(${a}.${ident(physical({ source, property: pk }))} IS NOT NULL)`,
2188
+ at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
2189
+ by: "CAST(NULL AS text)"
2190
+ };
2191
+ }
2192
+ const at = `${a}.${ident(cellColumnName(name, "at"))}`;
2193
+ const tombstoned2 = source.kind === "prefetch" ? ` AND NOT ${a}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName)}` : "";
2194
+ return { source: source.name, value, has: `(${at} IS NOT NULL${tombstoned2})`, at, by: `${a}.${ident(cellColumnName(name, "by"))}` };
2195
+ }
2196
+ function outranks({ i, j, key, direction, inclusive }) {
2197
+ const ki = i[key];
2198
+ const kj = j[key];
2199
+ const op = inclusive ? `${direction}=` : direction;
2200
+ 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}))`;
2201
+ }
2202
+ function rankedWinner({ candidates, key, direction }) {
2203
+ const branches = candidates.map((candidate, index) => {
2204
+ const beats = candidates.map((other, otherIndex) => otherIndex === index ? null : `(NOT ${other.has} OR ${outranks({ i: candidate, j: other, key, direction, inclusive: index < otherIndex })})`).filter((clause) => clause !== null);
2205
+ return `WHEN ${[candidate.has, ...beats].join(" AND ")} THEN ${index + 1}`;
2206
+ });
2207
+ return `CASE ${branches.join(" ")} END`;
2208
+ }
2209
+ function winnerIndex({ comparator, candidates }) {
2210
+ const firstWith = (test) => candidates.map((candidate, index) => `WHEN ${test(candidate)} THEN ${index + 1}`).join(" ");
2211
+ if (candidates.length === 1 || comparator === "priority") return `CASE ${firstWith((c) => c.has)} END`;
2212
+ switch (comparator) {
2213
+ case "latest":
2214
+ return rankedWinner({ candidates, key: "at", direction: ">" });
2215
+ case "highest":
2216
+ return rankedWinner({ candidates, key: "value", direction: ">" });
2217
+ case "lowest":
2218
+ return rankedWinner({ candidates, key: "value", direction: "<" });
2219
+ case "nonNullFirst":
2220
+ return `CASE ${firstWith((c) => `${c.has} AND ${c.value} IS NOT NULL`)} ${firstWith((c) => c.has)} END`;
2221
+ }
2222
+ }
2223
+ function pick(winner, candidates, field) {
2224
+ return `CASE ${winner} ${candidates.map((candidate, index) => `WHEN ${index + 1} THEN ${field(candidate)}`).join(" ")} END`;
2225
+ }
2226
+ function loserSql({ loser, winner }) {
2227
+ 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`;
2228
+ }
2229
+ function losersAgainst({ candidates, winner, isWinner }) {
2230
+ const losers = candidates.flatMap((loser, index) => {
2231
+ const excluded = isWinner(loser, index);
2232
+ if (excluded === true) return [];
2233
+ const clause = loserSql({ loser, winner });
2234
+ return [excluded === false ? clause : `CASE WHEN ${excluded} THEN '[]'::jsonb ELSE ${clause} END`];
2235
+ });
2236
+ return losers.length === 0 ? "CAST(NULL AS jsonb)" : `NULLIF(${losers.join(" || ")}, '[]'::jsonb)`;
2237
+ }
2238
+ function builtinCell({ comparator, candidates }) {
2239
+ const winner = winnerIndex({ comparator, candidates });
2240
+ const conflicts = comparator === "latest" || candidates.length < 2 ? "CAST(NULL AS jsonb)" : `CASE ${winner} ${candidates.map((known, index) => `WHEN ${index + 1} THEN ${losersAgainst({ candidates, winner: known, isWinner: (_c, other) => other === index })}`).join(" ")} END`;
2241
+ return {
2242
+ value: pick(winner, candidates, (c) => c.value),
2243
+ parts: {
2244
+ source: pick(winner, candidates, (c) => literal(c.source)),
2245
+ at: pick(winner, candidates, (c) => c.at),
2246
+ by: pick(winner, candidates, (c) => c.by),
2247
+ conflicts
2248
+ }
2249
+ };
2250
+ }
2251
+ function customCell({ candidates, orderBy, index }) {
2252
+ const w = ident(`w_${index}`);
2253
+ const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
2254
+ 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`;
2255
+ const winner = { value: `${w}.value`, at: `${w}.updated_at` };
2256
+ return {
2257
+ value: winner.value,
2258
+ lateral,
2259
+ parts: {
2260
+ source: `${w}.source`,
2261
+ at: winner.at,
2262
+ by: `${w}.actor`,
2263
+ conflicts: candidates.length < 2 ? "CAST(NULL AS jsonb)" : losersAgainst({ candidates, winner, isWinner: (c) => `${w}.source = ${literal(c.source)}` })
2264
+ }
2265
+ };
2266
+ }
2267
+ function buildResolvedQuery({ context, plan }) {
2268
+ const { qb, relation } = context;
2269
+ const pkColumn = plan.columns[plan.pk].columnNameFor(plan.pk);
2270
+ const alias = (source) => ident(`s_${source}`);
2271
+ const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
2272
+ const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
2273
+ const schema = pgCore.getTableConfig(relation(plan.plans[0].table)).schema;
2274
+ const keyBranches = plan.plans.map((source) => {
2275
+ const where = source.kind === "prefetch" ? drizzleOrm.sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : drizzleOrm.sql.raw("");
2276
+ const sourcePk = ident(physical({ source, property: plan.pk }));
2277
+ const selectedPk = sourcePk === ident(pkColumn) ? sourcePk : `${sourcePk} AS ${ident(pkColumn)}`;
2278
+ return drizzleOrm.sql`SELECT ${drizzleOrm.sql.raw(selectedPk)} FROM ${relation(source.table)}${where}`;
2279
+ });
2280
+ const joins = plan.plans.map(
2281
+ (source) => drizzleOrm.sql` LEFT JOIN ${relation(source.table)} AS ${drizzleOrm.sql.raw(alias(source.name))} ON ${drizzleOrm.sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
2282
+ );
2283
+ const cells = /* @__PURE__ */ new Map();
2284
+ plan.rules.forEach((rule, index) => {
2285
+ const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
2286
+ const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
2287
+ const candidates = rule.candidates.map((name) => candidateSql({ source: sourceByName.get(name), column: rule.column, pk: plan.pk, pgType }));
2288
+ cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index }) : builtinCell({ comparator: rule.comparator, candidates }));
2289
+ });
2290
+ const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [drizzleOrm.sql.raw(cell.lateral)] : []);
2291
+ const from = drizzleOrm.sql.join([drizzleOrm.sql`(${drizzleOrm.sql.join(keyBranches, drizzleOrm.sql.raw(" UNION "))}) AS "k"`, ...joins, ...laterals], drizzleOrm.sql.raw(""));
2292
+ const selected = {
2293
+ [plan.pk]: drizzleOrm.sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
2294
+ };
2295
+ for (const [property, builder] of Object.entries(plan.columns)) {
2296
+ if (property === plan.pk) continue;
2297
+ const cell = cells.get(property);
2298
+ const columnName = builder.columnNameFor(property);
2299
+ selected[property] = drizzleOrm.sql.raw(cell.value).as(columnName);
2300
+ for (const [part, expression] of Object.entries(cell.parts)) {
2301
+ selected[cellColumnName(property, part)] = drizzleOrm.sql.raw(expression).as(cellColumnName(columnName, part));
2302
+ }
2303
+ }
2304
+ const manual = plan.plans.find((source) => source.kind === "manual");
2305
+ const system = RESOLVED_VIEW_SYSTEM_COLUMNS;
2306
+ selected[system.writebackStatus.name] = drizzleOrm.sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName)}`).as(system.writebackStatus.columnName);
2307
+ selected[system.writebackError.name] = drizzleOrm.sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName)}`).as(system.writebackError.columnName);
2308
+ const hidden = plan.deletedBy.map((name) => `NOT COALESCE(${alias(name)}.${ident(tombstoned2)}, false)`);
2309
+ const query = qb.select(selected).from(from);
2310
+ return hidden.length > 0 ? query.where(drizzleOrm.sql.raw(hidden.join(" AND "))) : query;
2311
+ }
2312
+ function managedFn(id) {
2313
+ return { kind: "managed", id };
2314
+ }
2315
+ function manualSource() {
2316
+ return { kind: "manual" };
2317
+ }
2318
+ function etlSource(input) {
2319
+ return { kind: "etl", ...input };
2320
+ }
2321
+ function prefetchSource(input) {
2322
+ return { kind: "prefetch", ...input };
2323
+ }
2324
+ var latest = (candidates) => ({ kind: "latest", candidates });
2325
+ var highest = (candidates) => ({ kind: "highest", candidates });
2326
+ var lowest = (candidates) => ({ kind: "lowest", candidates });
2327
+ var priority = (candidates) => ({ kind: "priority", candidates });
2328
+ var nonNullFirst = (candidates) => ({ kind: "nonNullFirst", candidates });
2329
+ var custom = (candidates, orderBy) => ({ kind: "custom", candidates, orderBy });
2330
+ var SYSTEM_VIEW_COLUMNS = {
2331
+ [RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.columnName),
2332
+ [RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.columnName)
2333
+ };
2334
+ function defineResolvedTable(name, columns, config) {
2335
+ const file = activeAuthoringContext()?.currentFile;
2336
+ const fail = (message) => {
2337
+ throw new OrmDefinitionError(`resolved table "${name}": ${message}`, file);
2338
+ };
2339
+ if (!IDENTIFIER_PATTERN.test(name)) fail(`name must match ${IDENTIFIER_PATTERN}`);
2340
+ for (const property of Object.keys(columns)) {
2341
+ if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
2342
+ fail(`column "${property}" collides with a reserved system column`);
2343
+ }
2344
+ }
2345
+ for (const property of Object.keys(columns)) {
2346
+ if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
2347
+ fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
2348
+ }
2349
+ }
2350
+ const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
2351
+ if (pkEntries.length !== 1) fail("declare exactly one .primaryKey() column");
2352
+ const [pk, pkBuilder] = pkEntries[0];
2353
+ const dataColumns = Object.keys(columns).filter((property) => property !== pk);
2354
+ if (dataColumns.length === 0) fail("declare at least one non-key column");
2355
+ const sourceEntries = Object.entries(config.sources);
2356
+ if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
2357
+ const plans = sourceEntries.map(
2358
+ ([sourceName, input]) => planSource({ viewName: name, sourceName, input, columns, pk, pkBuilder, dataColumns, fail })
2359
+ );
2360
+ const rules = planRules({ config, plans, dataColumns, fail });
2361
+ const deletedBy = [...config.rows?.deletedBy ?? []];
2362
+ for (const source of deletedBy) {
2363
+ if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
2364
+ }
2365
+ const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
2366
+ const viewColumns = {};
2367
+ for (const [property, builder] of Object.entries(columns)) {
2368
+ viewColumns[property] = cloneColumn({ builder, property, notNull: property === pk, primaryKey: property === pk });
2369
+ if (property === pk) continue;
2370
+ const columnName = builder.columnNameFor(property);
2371
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
2372
+ viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
2373
+ }
2374
+ }
2375
+ for (const [property, make] of Object.entries(SYSTEM_VIEW_COLUMNS)) viewColumns[property] = make();
2376
+ const { read, ...writePolicies } = config.policies;
2377
+ const resolved = {
2378
+ internalTables: plans.filter((plan) => plan.kind !== "etl").map((plan) => plan.table),
2379
+ sources: plans,
2380
+ rules,
2381
+ deletedBy,
2382
+ policies: writePolicies
2383
+ };
2384
+ const queryPlan = { columns, pk, plans, rules, deletedBy };
2385
+ const typedColumns = viewColumns;
2386
+ const surface = {
2387
+ ...config.description !== void 0 ? { description: config.description } : {},
2388
+ ...config.links !== void 0 ? { links: config.links } : {},
2389
+ policies: { read }
2390
+ };
2391
+ if (!config.materialize) {
2392
+ return defineView(name, typedColumns, { ...surface, query: (context) => buildResolvedQuery({ context, plan: queryPlan }), resolved });
2393
+ }
2394
+ const materialize = config.materialize === true ? {} : config.materialize;
2395
+ const computeName = `${name}__compute`;
2396
+ const tableName = `${name}__materialized`;
2397
+ const compute = defineView(computeName, typedColumns, {
2398
+ policies: { read },
2399
+ query: (context) => buildResolvedQuery({ context, plan: queryPlan }),
2400
+ internal: true
2401
+ });
2402
+ const table = defineTable(tableName, viewColumns, {
2403
+ description: `Materialized rows of resolved table "${name}". System-maintained from "${computeName}".`,
2404
+ internal: true,
2405
+ materializes: name,
2406
+ ...materialize.indexes !== void 0 ? { indexes: materialize.indexes } : {}
2407
+ });
2408
+ return defineView(name, typedColumns, {
2409
+ ...surface,
2410
+ query: (context) => {
2411
+ const handle = context.relation(table);
2412
+ const selected = Object.fromEntries(Object.entries(viewColumns).map(([property, builder]) => [
2413
+ property,
2414
+ drizzleOrm.sql`${handle[property]}`.as(builder.columnNameFor(property))
2415
+ ]));
2416
+ return context.qb.select(selected).from(context.relation(table));
2417
+ },
2418
+ resolved: {
2419
+ ...resolved,
2420
+ internalTables: [...resolved.internalTables, table],
2421
+ internalViews: [compute],
2422
+ materialized: { table: tableName, compute: computeName }
2423
+ }
2424
+ });
2425
+ }
2426
+ function planRules({ config, plans, dataColumns, fail }) {
2427
+ const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
2428
+ const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
2429
+ for (const column of Object.keys(config.resolve ?? {})) {
2430
+ if (!dataColumns.includes(column)) fail(`resolve names unknown column "${column}"`);
2431
+ }
2432
+ return dataColumns.map((column) => {
2433
+ const explicit = config.resolve?.[column];
2434
+ const comparator = explicit ?? fallback;
2435
+ const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name) => plans.find((plan) => plan.name === name)?.columns.includes(column));
2436
+ for (const candidate of candidates) {
2437
+ const plan = plans.find((entry) => entry.name === candidate);
2438
+ if (!plan) fail(`column "${column}" names unknown source "${candidate}"`);
2439
+ else if (!plan.columns.includes(column)) fail(`column "${column}" candidate "${candidate}" does not supply it`);
2440
+ }
2441
+ if (candidates.length === 0) fail(`column "${column}" has no candidate source`);
2442
+ if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail });
2443
+ return {
2444
+ column,
2445
+ comparator: comparator.kind,
2446
+ candidates,
2447
+ ...comparator.kind === "custom" && comparator.orderBy !== void 0 ? { orderBy: comparator.orderBy } : {}
2448
+ };
2449
+ });
2450
+ }
2451
+ var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
2452
+ "value",
2453
+ "updated_at",
2454
+ "source",
2455
+ "actor",
2456
+ "ord",
2457
+ "asc",
2458
+ "desc",
2459
+ "nulls",
2460
+ "first",
2461
+ "last",
2462
+ "case",
2463
+ "when",
2464
+ "then",
2465
+ "else",
2466
+ "end",
2467
+ "and",
2468
+ "or",
2469
+ "not",
2470
+ "is",
2471
+ "null",
2472
+ "true",
2473
+ "false",
2474
+ "in",
2475
+ "distinct",
2476
+ "from",
2477
+ "coalesce",
2478
+ "nullif",
2479
+ "greatest",
2480
+ "least",
2481
+ "text",
2482
+ "numeric",
2483
+ "integer",
2484
+ "bigint",
2485
+ "boolean",
2486
+ "timestamptz",
2487
+ "date",
2488
+ "jsonb",
2489
+ "interval"
2490
+ ]);
2491
+ var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
2492
+ var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
2493
+ function assertSafeOrderBy({ orderBy, column, fail }) {
2494
+ ORDER_BY_TOKEN.lastIndex = 0;
2495
+ let consumed = 0;
2496
+ for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
2497
+ consumed = ORDER_BY_TOKEN.lastIndex;
2498
+ const token = match[0].toLowerCase();
2499
+ if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
2500
+ fail(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
2501
+ }
2502
+ }
2503
+ if (consumed !== orderBy.length || orderBy.trim() === "") {
2504
+ fail(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
2505
+ }
2506
+ }
1278
2507
  function stableJson(value) {
1279
2508
  if (value === null || typeof value !== "object") return JSON.stringify(value);
1280
2509
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -1303,8 +2532,19 @@ function defineNamespace(input) {
1303
2532
  return config;
1304
2533
  }
1305
2534
  function serializeNamespace(input) {
1306
- const { config, tables } = input;
1307
- const views = input.views ?? [];
2535
+ const { config } = input;
2536
+ const views = [...input.views ?? []];
2537
+ const tables = [...input.tables];
2538
+ for (const view of views) {
2539
+ for (const internal of view.config.resolved?.internalTables ?? []) {
2540
+ if (!tables.includes(internal)) tables.push(internal);
2541
+ }
2542
+ }
2543
+ for (const view of [...views]) {
2544
+ for (const internal of view.config.resolved?.internalViews ?? []) {
2545
+ if (!views.includes(internal)) views.push(internal);
2546
+ }
2547
+ }
1308
2548
  const enums = input.enums ?? [];
1309
2549
  const tablesByName = /* @__PURE__ */ new Map();
1310
2550
  for (const table of tables) {
@@ -1406,6 +2646,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1406
2646
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
1407
2647
  if (table.source !== void 0) candidate.source = table.source;
1408
2648
  if (table.source !== void 0) {
2649
+ if (table.config.checks?.length) {
2650
+ throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
2651
+ }
1409
2652
  if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
1410
2653
  throw new OrmDefinitionError(
1411
2654
  `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
@@ -1443,6 +2686,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1443
2686
  if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1444
2687
  candidate.columns?.push(column);
1445
2688
  }
2689
+ if (table.config.internal === true) candidate.internal = true;
2690
+ if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
2691
+ if (table.config.cellMetadata === true && table.source !== void 0) {
2692
+ throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
2693
+ }
1446
2694
  if (table.config.primaryKey !== void 0) {
1447
2695
  if (columnLevelPk.length > 0) {
1448
2696
  throw new OrmDefinitionError(
@@ -1456,6 +2704,24 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1456
2704
  } else if (columnLevelPk.length > 0) {
1457
2705
  candidate.primaryKey = columnLevelPk;
1458
2706
  }
2707
+ if (table.config.cellMetadata === true) {
2708
+ candidate.cellMetadata = true;
2709
+ const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
2710
+ const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
2711
+ for (const column of tracked) {
2712
+ for (const part of parts) {
2713
+ candidate.columns?.push({
2714
+ name: cellColumnName(column.name, part),
2715
+ columnName: cellColumnName(column.columnName, part),
2716
+ type: CELL_PART_TYPES[part],
2717
+ nullable: true,
2718
+ unique: false,
2719
+ deprecated: false,
2720
+ cell: { of: column.name, part }
2721
+ });
2722
+ }
2723
+ }
2724
+ }
1459
2725
  if (table.config.foreignKeys?.length) {
1460
2726
  if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
1461
2727
  candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
@@ -1468,10 +2734,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1468
2734
  columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
1469
2735
  target: target.tableName,
1470
2736
  references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
1471
- ...fk.deferrable ? { deferrable: fk.deferrable } : {}
2737
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {},
2738
+ ...fk.onDelete ? { onDelete: fk.onDelete } : {},
2739
+ ...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
1472
2740
  };
1473
2741
  });
1474
2742
  }
2743
+ if (table.config.checks?.length) {
2744
+ candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
2745
+ }
1475
2746
  for (const index of table.config.indexes ?? []) {
1476
2747
  const entry = {
1477
2748
  columns: index.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1567,7 +2838,33 @@ function buildCanonicalView({
1567
2838
  columns,
1568
2839
  ...primaryKey !== void 0 ? { primaryKey } : {},
1569
2840
  links,
1570
- policies: { read: toCanonicalPolicy(view.config.policies.read) }
2841
+ policies: { read: toCanonicalPolicy(view.config.policies.read) },
2842
+ ...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
2843
+ ...view.config.internal === true ? { internal: true } : {}
2844
+ };
2845
+ }
2846
+ function buildCanonicalResolved(resolved) {
2847
+ const policies = {};
2848
+ if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
2849
+ if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
2850
+ if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
2851
+ return {
2852
+ sources: resolved.sources.map((source) => ({
2853
+ name: source.name,
2854
+ kind: source.kind,
2855
+ table: source.table.tableName,
2856
+ columns: [...source.columns],
2857
+ ...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
2858
+ ...source.key !== void 0 ? { key: source.key } : {},
2859
+ ...source.fetch !== void 0 ? { fetch: source.fetch } : {},
2860
+ ...source.function !== void 0 ? { function: { ...source.function } } : {},
2861
+ ...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
2862
+ ...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
2863
+ })),
2864
+ resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
2865
+ deletedBy: [...resolved.deletedBy],
2866
+ ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
2867
+ policies
1571
2868
  };
1572
2869
  }
1573
2870
  function buildCanonicalLink({
@@ -1584,6 +2881,9 @@ function buildCanonicalLink({
1584
2881
  if (link.enforce && link.linkKind !== "toOne") {
1585
2882
  return fail("sets enforce: true, which is only supported on toOne links");
1586
2883
  }
2884
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
2885
+ return fail("sets a referential action without enforce: true");
2886
+ }
1587
2887
  if (link.linkKind === "toPlatformUser") {
1588
2888
  if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
1589
2889
  return {
@@ -1612,6 +2912,8 @@ function buildCanonicalLink({
1612
2912
  on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1613
2913
  ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
1614
2914
  enforce: link.enforce,
2915
+ ...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
2916
+ ...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
1615
2917
  ...link.description !== void 0 ? { description: link.description } : {}
1616
2918
  };
1617
2919
  }
@@ -1885,23 +3187,34 @@ exports.activeAuthoringContext = activeAuthoringContext;
1885
3187
  exports.beginAuthoringContext = beginAuthoringContext;
1886
3188
  exports.computeCanonicalHash = computeCanonicalHash;
1887
3189
  exports.computeSyncedContractHash = computeSyncedContractHash;
3190
+ exports.custom = custom;
1888
3191
  exports.defineEnum = defineEnum;
1889
3192
  exports.defineNamespace = defineNamespace;
3193
+ exports.defineResolvedTable = defineResolvedTable;
1890
3194
  exports.defineSyncedTable = defineSyncedTable;
1891
3195
  exports.defineTable = defineTable;
1892
3196
  exports.defineView = defineView;
1893
3197
  exports.deriveColumnName = deriveColumnName;
1894
3198
  exports.endAuthoringContext = endAuthoringContext;
3199
+ exports.etlSource = etlSource;
1895
3200
  exports.extractOrmSyncedTableContract = extractOrmSyncedTableContract;
3201
+ exports.highest = highest;
1896
3202
  exports.isColumnBuilder = isColumnBuilder;
1897
3203
  exports.isEnumRef = isEnumRef;
1898
3204
  exports.isSeqLink = isSeqLink;
1899
3205
  exports.isSeqNamespaceConfig = isSeqNamespaceConfig;
1900
3206
  exports.isSeqTable = isSeqTable;
1901
3207
  exports.isSeqView = isSeqView;
3208
+ exports.latest = latest;
1902
3209
  exports.loadNamespaceFromDir = loadNamespaceFromDir;
3210
+ exports.lowest = lowest;
3211
+ exports.managedFn = managedFn;
3212
+ exports.manualSource = manualSource;
3213
+ exports.nonNullFirst = nonNullFirst;
1903
3214
  exports.overlayWinsUnion = overlayWinsUnion;
1904
3215
  exports.overrideRowMutation = overrideRowMutation;
3216
+ exports.prefetchSource = prefetchSource;
3217
+ exports.priority = priority;
1905
3218
  exports.revertOverlayMutation = revertOverlayMutation;
1906
3219
  exports.serializeNamespace = serializeNamespace;
1907
3220
  exports.syncedContractSurface = syncedContractSurface;