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