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