@sequenceholdings/orm 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +94 -2
  2. package/dist/authoring.cjs +1474 -54
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +242 -4
  5. package/dist/authoring.d.ts +242 -4
  6. package/dist/authoring.js +1465 -56
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +740 -42
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +742 -53
  11. package/dist/canonical.d.ts +742 -53
  12. package/dist/canonical.js +724 -43
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +2478 -328
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +2480 -330
  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 +1755 -329
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +56 -3
  27. package/dist/compiler.d.ts +56 -3
  28. package/dist/compiler.js +1756 -330
  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 +1973 -164
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +474 -100
  37. package/dist/index.d.ts +474 -100
  38. package/dist/index.js +1940 -167
  39. package/dist/index.js.map +1 -1
  40. package/dist/ir.cjs +869 -46
  41. package/dist/ir.cjs.map +1 -1
  42. package/dist/ir.d.cts +699 -25
  43. package/dist/ir.d.ts +699 -25
  44. package/dist/ir.js +861 -47
  45. package/dist/ir.js.map +1 -1
  46. package/dist/{naming-CTvWSRTf.d.ts → naming-CPfx7EfF.d.ts} +28 -1
  47. package/dist/{naming-DRyinU4u.d.cts → naming-CU9ThP_E.d.cts} +28 -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 +852 -49
  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 +852 -49
  55. package/dist/predicate-v2.js.map +1 -1
  56. package/dist/sdl.cjs +1309 -58
  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 +1305 -59
  61. package/dist/sdl.js.map +1 -1
  62. package/package.json +11 -6
  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';
@@ -30,10 +30,12 @@ var ColumnBuilder = class {
30
30
  hasDefault: false,
31
31
  defaultValue: void 0,
32
32
  defaultFn: void 0,
33
+ onUpdateFn: void 0,
33
34
  description: void 0,
34
35
  renamedFrom: void 0,
35
36
  cast: void 0,
36
- deprecated: false
37
+ deprecated: false,
38
+ cell: void 0
37
39
  };
38
40
  }
39
41
  /** Marks this column as (part of) the primary key. Implies NOT NULL. */
@@ -65,6 +67,15 @@ var ColumnBuilder = class {
65
67
  this.state.defaultFn = void 0;
66
68
  return this;
67
69
  }
70
+ /**
71
+ * Stamp this column with Postgres `now()` on every UPDATE. Only valid on
72
+ * timestamptz/date; the compiler drops it from SetInput so callers cannot
73
+ * supply a value.
74
+ */
75
+ onUpdate(value) {
76
+ this.state.onUpdateFn = value;
77
+ return this;
78
+ }
68
79
  description(text2) {
69
80
  this.state.description = text2;
70
81
  return this;
@@ -84,6 +95,11 @@ var ColumnBuilder = class {
84
95
  this.state.deprecated = true;
85
96
  return this;
86
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
+ }
87
103
  /** Whether `.primaryKey()` was called — collected into the table-level PK. */
88
104
  get isPrimaryKey() {
89
105
  return this.state.primaryKey;
@@ -108,9 +124,11 @@ var ColumnBuilder = class {
108
124
  };
109
125
  if (this.state.hasDefault) column.default = this.state.defaultValue ?? null;
110
126
  if (this.state.defaultFn !== void 0) column.defaultFn = this.state.defaultFn;
127
+ if (this.state.onUpdateFn !== void 0) column.onUpdateFn = this.state.onUpdateFn;
111
128
  if (this.state.description !== void 0) column.description = this.state.description;
112
129
  if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
113
130
  if (this.state.cast !== void 0) column.cast = this.state.cast;
131
+ if (this.state.cell !== void 0) column.cell = { ...this.state.cell };
114
132
  return column;
115
133
  }
116
134
  };
@@ -135,9 +153,25 @@ var t = {
135
153
  // src/constants.ts
136
154
  var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
137
155
  var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
156
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
138
157
  var MAX_IDENTIFIER_LENGTH = 63;
139
158
  var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
140
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: [] });
141
175
 
142
176
  // src/errors.ts
143
177
  var OrmDefinitionError = class extends Error {
@@ -206,6 +240,8 @@ function toOne(target, options) {
206
240
  on: options.on,
207
241
  references: options.references,
208
242
  enforce: options.enforce ?? false,
243
+ onDelete: options.onDelete,
244
+ onUpdate: options.onUpdate,
209
245
  description: options.description
210
246
  };
211
247
  }
@@ -217,6 +253,8 @@ function toMany(target, options) {
217
253
  on: options.on,
218
254
  references: options.references,
219
255
  enforce: false,
256
+ onDelete: void 0,
257
+ onUpdate: void 0,
220
258
  description: options.description
221
259
  };
222
260
  }
@@ -228,6 +266,8 @@ function toPlatformUser(options) {
228
266
  on: options.on,
229
267
  references: options.references,
230
268
  enforce: false,
269
+ onDelete: void 0,
270
+ onUpdate: void 0,
231
271
  description: options.description
232
272
  };
233
273
  }
@@ -237,10 +277,10 @@ function isSeqLink(value) {
237
277
  function isSeqTable(value) {
238
278
  return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
239
279
  }
240
- function assertIdentifier({ value, what, file }) {
241
- if (!IDENTIFIER_PATTERN.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
280
+ function assertIdentifier({ value, what, file, pattern = IDENTIFIER_PATTERN }) {
281
+ if (!pattern.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
242
282
  throw new OrmDefinitionError(
243
- `${what} "${value}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
283
+ `${what} "${value}" must match ${pattern} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
244
284
  file
245
285
  );
246
286
  }
@@ -261,7 +301,13 @@ function defineTable(name, columns, config = {}) {
261
301
  );
262
302
  }
263
303
  const columnName = builder.columnNameFor(propertyName);
264
- assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file });
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
+ }
265
311
  const existing = seenColumnNames.get(columnName);
266
312
  if (existing !== void 0) {
267
313
  throw new OrmDefinitionError(
@@ -301,7 +347,8 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
301
347
  assertIdentifier({
302
348
  value: columnName,
303
349
  what: `synced table "${name}": column "${propertyName}" column name`,
304
- file
350
+ file,
351
+ pattern: COLUMN_IDENTIFIER_PATTERN
305
352
  });
306
353
  const existing = seenColumnNames.get(columnName);
307
354
  if (existing !== void 0) {
@@ -390,7 +437,7 @@ function defineView(name, columns, config) {
390
437
  );
391
438
  }
392
439
  const columnName = builder.columnNameFor(propertyName);
393
- if (!IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
440
+ if (!COLUMN_IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
394
441
  throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
395
442
  }
396
443
  const existing = seenColumnNames.get(columnName);
@@ -508,11 +555,11 @@ function compileViewQueries(input) {
508
555
  );
509
556
  }
510
557
  if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
511
- const tableDependencies = extractUsedTable(config.query).map((physical) => {
512
- const logical = physicalToLogical.get(physical);
558
+ const tableDependencies = extractUsedTable(config.query).map((physical2) => {
559
+ const logical = physicalToLogical.get(physical2);
513
560
  if (!logical) {
514
561
  throw new OrmDefinitionError(
515
- `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`,
516
563
  authored.file
517
564
  );
518
565
  }
@@ -836,9 +883,469 @@ function scalarColumnType(table, property, helper) {
836
883
  }
837
884
  return type;
838
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
839
1345
  var CANONICAL_FORMAT = 2;
840
1346
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
841
- var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
1347
+ var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
1348
+ var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
842
1349
  var COLUMN_TYPES = [
843
1350
  "uuid",
844
1351
  "text",
@@ -853,59 +1360,71 @@ var COLUMN_TYPES = [
853
1360
  ];
854
1361
  var columnTypeSchema = z.enum(COLUMN_TYPES);
855
1362
  var canonicalEnumSchema = z.object({
856
- name: identifier,
1363
+ name: identifier2,
857
1364
  values: z.array(z.string().min(1).max(63)).min(1).max(100)
858
1365
  });
859
1366
  var canonicalColumnSchema = z.object({
860
1367
  /** Property name in TS (camelCase); columnName is the Postgres name. */
861
1368
  name: z.string().min(1).max(63),
862
- columnName: identifier,
863
- type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
1369
+ columnName: columnIdentifier,
1370
+ type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
864
1371
  nullable: z.boolean().default(true),
865
1372
  unique: z.boolean().default(false),
866
1373
  /** Literal default, or one of the allowlisted SQL functions. */
867
1374
  default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
868
1375
  defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
1376
+ /**
1377
+ * Stamp this column from the database clock on UPDATE (`now()`). The
1378
+ * column stays insertable (defaults fill gaps) but is omitted from
1379
+ * SetInput / settable so callers cannot backdate it.
1380
+ */
1381
+ onUpdateFn: z.enum(["now()"]).optional(),
869
1382
  description: z.string().max(500).optional(),
870
1383
  /** Migration hints — same semantics as v1. */
871
- renamedFrom: identifier.optional(),
1384
+ renamedFrom: columnIdentifier.optional(),
872
1385
  cast: z.string().max(200).optional(),
873
- 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()
1394
+ }).superRefine((column, ctx) => {
1395
+ if (column.onUpdateFn === void 0) return;
1396
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
1397
+ ctx.addIssue({
1398
+ code: "custom",
1399
+ path: ["onUpdateFn"],
1400
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
1401
+ });
1402
+ }
874
1403
  });
875
1404
  var canonicalLinkSchema = z.object({
876
1405
  name: z.string().min(1).max(63),
877
1406
  kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
878
1407
  /** Target table name (namespace-local). Absent for toPlatformUser. */
879
- target: identifier.optional(),
1408
+ target: identifier2.optional(),
880
1409
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
881
- on: identifier,
1410
+ on: columnIdentifier,
882
1411
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
883
- references: identifier.optional(),
1412
+ references: columnIdentifier.optional(),
884
1413
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
885
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(),
886
1417
  description: z.string().max(500).optional()
887
1418
  });
888
1419
  var canonicalIndexSchema = z.object({
889
- columns: z.array(identifier).min(1),
1420
+ columns: z.array(columnIdentifier).min(1),
890
1421
  unique: z.boolean().default(false),
891
- name: identifier.optional()
1422
+ name: identifier2.optional()
892
1423
  });
893
- var policyVariantSchema = z.object({
894
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
895
- when: z.string().min(1).max(1e3),
896
- /** Exactly one of: allow-all bypass, or a row predicate. */
897
- allow: z.literal(true).optional(),
898
- policy: z.string().min(1).max(2e3).optional()
899
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
900
- message: "A variant declares exactly one of allow: true or policy"
901
- });
902
- var policyExpressionSchema = z.union([
903
- z.string().min(1).max(2e3),
904
- z.object({
905
- default: z.string().min(1).max(2e3),
906
- variants: z.array(policyVariantSchema).max(10).default([])
907
- })
908
- ]);
1424
+ var canonicalCheckSchema = z.object({
1425
+ name: identifier2,
1426
+ expression: z.string().min(1).max(2e3)
1427
+ }).strict();
909
1428
  var canonicalPoliciesSchema = z.object({
910
1429
  read: policyExpressionSchema.optional(),
911
1430
  write: policyExpressionSchema.optional(),
@@ -935,8 +1454,165 @@ function isSingleViewQuerySql(value) {
935
1454
  }
936
1455
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
937
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
+ ];
938
1614
  var canonicalActionSchema = z.object({
939
- name: identifier,
1615
+ name: identifier2,
940
1616
  requires: z.string().max(1e3).optional(),
941
1617
  params: z.record(z.string(), z.object({
942
1618
  type: columnTypeSchema,
@@ -948,9 +1624,9 @@ var canonicalActionSchema = z.object({
948
1624
  });
949
1625
  var canonicalCowMutationSchema = z.object({
950
1626
  kind: z.enum(["override", "revert"]),
951
- name: identifier,
1627
+ name: identifier2,
952
1628
  /** Synced table (same namespace) the overlay copies from / falls back to. */
953
- synced: identifier,
1629
+ synced: identifier2,
954
1630
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
955
1631
  pk: z.string().min(1).max(63),
956
1632
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -977,17 +1653,28 @@ var canonicalCowMutationSchema = z.object({
977
1653
  }
978
1654
  }
979
1655
  });
1656
+ var canonicalForeignKeySchema = z.object({
1657
+ name: identifier2,
1658
+ columns: z.array(columnIdentifier).min(1).max(8),
1659
+ target: identifier2,
1660
+ references: z.array(columnIdentifier).min(1).max(8),
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()
1664
+ }).strict();
980
1665
  var canonicalTableSchema = z.object({
981
- table: identifier,
1666
+ table: identifier2,
982
1667
  description: z.string().max(2e3).optional(),
983
1668
  /** Table-level rename hint (new in v2). */
984
- renamedFrom: identifier.optional(),
1669
+ renamedFrom: identifier2.optional(),
985
1670
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
986
1671
  source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
987
1672
  columns: z.array(canonicalColumnSchema).default([]),
988
- primaryKey: z.array(identifier).min(1).max(8).optional(),
1673
+ primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
989
1674
  links: z.array(canonicalLinkSchema).default([]),
990
1675
  indexes: z.array(canonicalIndexSchema).default([]),
1676
+ /** Omitted by old definitions, preserving their existing content hashes. */
1677
+ checks: z.array(canonicalCheckSchema).max(100).optional(),
991
1678
  policies: canonicalPoliciesSchema.default({}),
992
1679
  /**
993
1680
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -996,26 +1683,64 @@ var canonicalTableSchema = z.object({
996
1683
  */
997
1684
  actions: z.array(canonicalActionSchema).default([]),
998
1685
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
999
- cowMutations: z.array(canonicalCowMutationSchema).optional()
1686
+ cowMutations: z.array(canonicalCowMutationSchema).optional(),
1687
+ /** Omitted by old definitions, preserving their existing content hashes. */
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()
1000
1709
  });
1710
+ var MAX_VIEW_QUERY = 1e5;
1711
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
1001
1712
  var canonicalViewSchema = z.object({
1002
- view: identifier,
1713
+ view: identifier2,
1003
1714
  description: z.string().max(2e3).optional(),
1004
1715
  columns: z.array(canonicalColumnSchema).min(1),
1005
1716
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
1006
- primaryKey: z.array(identifier).min(1).max(8).optional(),
1717
+ primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
1007
1718
  links: z.array(canonicalLinkSchema).default([]),
1008
1719
  policies: z.object({ read: policyExpressionSchema }).strict(),
1009
- query: z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
1720
+ query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
1010
1721
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
1011
1722
  }),
1012
1723
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
1013
- 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()
1014
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
+ }
1015
1737
  for (const [index, column] of view.columns.entries()) {
1016
1738
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1017
1739
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
1018
1740
  }
1741
+ if (column.onUpdateFn !== void 0) {
1742
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
1743
+ }
1019
1744
  if (column.unique) {
1020
1745
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
1021
1746
  }
@@ -1023,15 +1748,82 @@ var canonicalViewSchema = z.object({
1023
1748
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
1024
1749
  }
1025
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
+ }
1026
1756
  for (const [index, link] of view.links.entries()) {
1027
1757
  if (link.enforce) {
1028
1758
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
1029
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
+ }
1030
1763
  }
1031
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
+ }
1032
1824
  var canonicalDefinitionSchema = z.object({
1033
1825
  definition_format: z.literal(CANONICAL_FORMAT),
1034
- namespace: identifier.max(41),
1826
+ namespace: identifier2.max(41),
1035
1827
  description: z.string().max(2e3).optional(),
1036
1828
  enums: z.array(canonicalEnumSchema).default([]),
1037
1829
  tables: z.array(canonicalTableSchema).min(0).max(500),
@@ -1049,6 +1841,74 @@ var canonicalDefinitionSchema = z.object({
1049
1841
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
1050
1842
  }
1051
1843
  seen.add(table.table);
1844
+ validateCellMetadata(table, index, ctx);
1845
+ if (table.source !== void 0) {
1846
+ for (const [columnIndex, column] of table.columns.entries()) {
1847
+ if (column.onUpdateFn !== void 0) {
1848
+ ctx.addIssue({
1849
+ code: "custom",
1850
+ path: ["tables", index, "columns", columnIndex],
1851
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
1852
+ });
1853
+ }
1854
+ }
1855
+ }
1856
+ }
1857
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
1858
+ for (const [tableIndex, table] of definition.tables.entries()) {
1859
+ const names = /* @__PURE__ */ new Set([
1860
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
1861
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
1862
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
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
+ }
1884
+ for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
1885
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
1886
+ if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
1887
+ names.add(fk.name);
1888
+ const target = fkTables.get(fk.target);
1889
+ if (table.source || !target || target.source) {
1890
+ fail("foreign keys must connect owned tables in the same namespace");
1891
+ continue;
1892
+ }
1893
+ if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
1894
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
1895
+ for (const [position, name] of fk.columns.entries()) {
1896
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
1897
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
1898
+ if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
1899
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
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
+ }
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])];
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");
1906
+ }
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
+ }
1052
1912
  }
1053
1913
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
1054
1914
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -1171,12 +2031,14 @@ function normalizeCanonical(definition) {
1171
2031
  ...definition,
1172
2032
  enums: sortByName(definition.enums),
1173
2033
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
1174
- const { cowMutations, ...rest } = t2;
2034
+ const { cowMutations, foreignKeys, checks, ...rest } = t2;
1175
2035
  return {
1176
2036
  ...rest,
1177
2037
  links: sortByName(t2.links),
1178
2038
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
1179
2039
  actions: sortByName(t2.actions),
2040
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
2041
+ ...checks?.length ? { checks: sortByName(checks) } : {},
1180
2042
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
1181
2043
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
1182
2044
  };
@@ -1191,6 +2053,454 @@ function normalizeCanonical(definition) {
1191
2053
  }
1192
2054
  return normalized;
1193
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
+ }
1194
2504
  function stableJson(value) {
1195
2505
  if (value === null || typeof value !== "object") return JSON.stringify(value);
1196
2506
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -1219,8 +2529,19 @@ function defineNamespace(input) {
1219
2529
  return config;
1220
2530
  }
1221
2531
  function serializeNamespace(input) {
1222
- const { config, tables } = input;
1223
- 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
+ }
1224
2545
  const enums = input.enums ?? [];
1225
2546
  const tablesByName = /* @__PURE__ */ new Map();
1226
2547
  for (const table of tables) {
@@ -1322,6 +2643,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1322
2643
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
1323
2644
  if (table.source !== void 0) candidate.source = table.source;
1324
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
+ }
1325
2649
  if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
1326
2650
  throw new OrmDefinitionError(
1327
2651
  `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
@@ -1359,6 +2683,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1359
2683
  if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1360
2684
  candidate.columns?.push(column);
1361
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
+ }
1362
2691
  if (table.config.primaryKey !== void 0) {
1363
2692
  if (columnLevelPk.length > 0) {
1364
2693
  throw new OrmDefinitionError(
@@ -1372,6 +2701,45 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1372
2701
  } else if (columnLevelPk.length > 0) {
1373
2702
  candidate.primaryKey = columnLevelPk;
1374
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
+ }
2722
+ if (table.config.foreignKeys?.length) {
2723
+ if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
2724
+ candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
2725
+ const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
2726
+ throw new OrmDefinitionError(message, table.file);
2727
+ } });
2728
+ if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
2729
+ return {
2730
+ name: fk.name,
2731
+ columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
2732
+ target: target.tableName,
2733
+ references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
2734
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {},
2735
+ ...fk.onDelete ? { onDelete: fk.onDelete } : {},
2736
+ ...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
2737
+ };
2738
+ });
2739
+ }
2740
+ if (table.config.checks?.length) {
2741
+ candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
2742
+ }
1375
2743
  for (const index of table.config.indexes ?? []) {
1376
2744
  const entry = {
1377
2745
  columns: index.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1424,6 +2792,12 @@ function buildCanonicalView({
1424
2792
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1425
2793
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1426
2794
  }
2795
+ if (column.onUpdateFn !== void 0) {
2796
+ throw new OrmDefinitionError(
2797
+ `view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
2798
+ view.file
2799
+ );
2800
+ }
1427
2801
  if (column.unique) {
1428
2802
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1429
2803
  }
@@ -1461,7 +2835,33 @@ function buildCanonicalView({
1461
2835
  columns,
1462
2836
  ...primaryKey !== void 0 ? { primaryKey } : {},
1463
2837
  links,
1464
- 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
1465
2865
  };
1466
2866
  }
1467
2867
  function buildCanonicalLink({
@@ -1478,6 +2878,9 @@ function buildCanonicalLink({
1478
2878
  if (link.enforce && link.linkKind !== "toOne") {
1479
2879
  return fail("sets enforce: true, which is only supported on toOne links");
1480
2880
  }
2881
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
2882
+ return fail("sets a referential action without enforce: true");
2883
+ }
1481
2884
  if (link.linkKind === "toPlatformUser") {
1482
2885
  if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
1483
2886
  return {
@@ -1506,6 +2909,8 @@ function buildCanonicalLink({
1506
2909
  on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1507
2910
  ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
1508
2911
  enforce: link.enforce,
2912
+ ...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
2913
+ ...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
1509
2914
  ...link.description !== void 0 ? { description: link.description } : {}
1510
2915
  };
1511
2916
  }
@@ -1684,6 +3089,10 @@ async function loadNamespaceFromDir(dir, options = {}) {
1684
3089
  fsCache: false,
1685
3090
  ...options.ormModulePath !== void 0 ? {
1686
3091
  alias: {
3092
+ // The public /drizzle barrel re-exports this pinned dependency.
3093
+ // Explicitly map it before the root alias: otherwise jiti appends
3094
+ // "/drizzle" to the overridden module's filename.
3095
+ "@sequenceholdings/orm/drizzle": createRequire(import.meta.url).resolve("drizzle-orm"),
1687
3096
  "@sequenceholdings/orm": options.ormModulePath,
1688
3097
  "@sequenceholdings/orm/authoring": options.ormModulePath
1689
3098
  }
@@ -1759,6 +3168,6 @@ function purgeModuleCache(root) {
1759
3168
  }
1760
3169
  }
1761
3170
 
1762
- 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 };
1763
3172
  //# sourceMappingURL=authoring.js.map
1764
3173
  //# sourceMappingURL=authoring.js.map