@sequenceholdings/orm 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +94 -2
  2. package/dist/authoring.cjs +1474 -54
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +242 -4
  5. package/dist/authoring.d.ts +242 -4
  6. package/dist/authoring.js +1465 -56
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +740 -42
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +742 -53
  11. package/dist/canonical.d.ts +742 -53
  12. package/dist/canonical.js +724 -43
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +2478 -328
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +2480 -330
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs +90 -25
  19. package/dist/client.cjs.map +1 -1
  20. package/dist/client.d.cts +39 -22
  21. package/dist/client.d.ts +39 -22
  22. package/dist/client.js +87 -26
  23. package/dist/client.js.map +1 -1
  24. package/dist/compiler.cjs +1755 -329
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +56 -3
  27. package/dist/compiler.d.ts +56 -3
  28. package/dist/compiler.js +1756 -330
  29. package/dist/compiler.js.map +1 -1
  30. package/dist/constants-YoFGjIBM.d.cts +99 -0
  31. package/dist/constants-YoFGjIBM.d.ts +99 -0
  32. package/dist/dynamic-input.d.cts +1 -0
  33. package/dist/dynamic-input.d.ts +1 -0
  34. package/dist/index.cjs +1973 -164
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +474 -100
  37. package/dist/index.d.ts +474 -100
  38. package/dist/index.js +1940 -167
  39. package/dist/index.js.map +1 -1
  40. package/dist/ir.cjs +869 -46
  41. package/dist/ir.cjs.map +1 -1
  42. package/dist/ir.d.cts +699 -25
  43. package/dist/ir.d.ts +699 -25
  44. package/dist/ir.js +861 -47
  45. package/dist/ir.js.map +1 -1
  46. package/dist/{naming-CTvWSRTf.d.ts → naming-CPfx7EfF.d.ts} +28 -1
  47. package/dist/{naming-DRyinU4u.d.cts → naming-CU9ThP_E.d.cts} +28 -1
  48. package/dist/policy-1iZbGnZZ.d.cts +24 -0
  49. package/dist/policy-1iZbGnZZ.d.ts +24 -0
  50. package/dist/predicate-v2.cjs +852 -49
  51. package/dist/predicate-v2.cjs.map +1 -1
  52. package/dist/predicate-v2.d.cts +2 -1
  53. package/dist/predicate-v2.d.ts +2 -1
  54. package/dist/predicate-v2.js +852 -49
  55. package/dist/predicate-v2.js.map +1 -1
  56. package/dist/sdl.cjs +1309 -58
  57. package/dist/sdl.cjs.map +1 -1
  58. package/dist/sdl.d.cts +7 -3
  59. package/dist/sdl.d.ts +7 -3
  60. package/dist/sdl.js +1305 -59
  61. package/dist/sdl.js.map +1 -1
  62. package/package.json +11 -6
  63. package/templates/orm-namespace/CLAUDE.md +6 -4
  64. package/templates/orm-namespace/codegen.preset.d.cts +7 -0
  65. package/templates/orm-namespace/codegen.ts +2 -1
  66. package/templates/orm-namespace/graphql/example/queries.ts +4 -3
  67. package/templates/orm-namespace/package.json +2 -1
  68. package/templates/orm-namespace/schema/tables.ts +3 -2
  69. package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { sql, eq, notExists } from 'drizzle-orm';
2
- import { pgSchema, uniqueIndex, index, foreignKey, primaryKey, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
3
- import { createHash } from 'crypto';
1
+ import { sql, eq, notExists, is, SQL } from 'drizzle-orm';
2
+ import { pgSchema, uniqueIndex, index, foreignKey, check, primaryKey, getTableConfig, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
4
3
  import { z } from 'zod';
4
+ import { createHash } from 'crypto';
5
5
  import { createJiti } from 'jiti';
6
6
  import { realpath, stat, readdir, readFile, mkdir, writeFile } from 'fs/promises';
7
7
  import { createRequire } from 'module';
@@ -34,10 +34,12 @@ var ColumnBuilder = class {
34
34
  hasDefault: false,
35
35
  defaultValue: void 0,
36
36
  defaultFn: void 0,
37
+ onUpdateFn: void 0,
37
38
  description: void 0,
38
39
  renamedFrom: void 0,
39
40
  cast: void 0,
40
- deprecated: false
41
+ deprecated: false,
42
+ cell: void 0
41
43
  };
42
44
  }
43
45
  /** Marks this column as (part of) the primary key. Implies NOT NULL. */
@@ -69,6 +71,15 @@ var ColumnBuilder = class {
69
71
  this.state.defaultFn = void 0;
70
72
  return this;
71
73
  }
74
+ /**
75
+ * Stamp this column with Postgres `now()` on every UPDATE. Only valid on
76
+ * timestamptz/date; the compiler drops it from SetInput so callers cannot
77
+ * supply a value.
78
+ */
79
+ onUpdate(value) {
80
+ this.state.onUpdateFn = value;
81
+ return this;
82
+ }
72
83
  description(text4) {
73
84
  this.state.description = text4;
74
85
  return this;
@@ -88,6 +99,11 @@ var ColumnBuilder = class {
88
99
  this.state.deprecated = true;
89
100
  return this;
90
101
  }
102
+ /** @internal System cell-metadata part of logical column `of` (generated resolved views only). */
103
+ cellPart(of, part) {
104
+ this.state.cell = { of, part };
105
+ return this;
106
+ }
91
107
  /** Whether `.primaryKey()` was called — collected into the table-level PK. */
92
108
  get isPrimaryKey() {
93
109
  return this.state.primaryKey;
@@ -112,9 +128,11 @@ var ColumnBuilder = class {
112
128
  };
113
129
  if (this.state.hasDefault) column.default = this.state.defaultValue ?? null;
114
130
  if (this.state.defaultFn !== void 0) column.defaultFn = this.state.defaultFn;
131
+ if (this.state.onUpdateFn !== void 0) column.onUpdateFn = this.state.onUpdateFn;
115
132
  if (this.state.description !== void 0) column.description = this.state.description;
116
133
  if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
117
134
  if (this.state.cast !== void 0) column.cast = this.state.cast;
135
+ if (this.state.cell !== void 0) column.cell = { ...this.state.cell };
118
136
  return column;
119
137
  }
120
138
  };
@@ -138,11 +156,12 @@ var t = {
138
156
 
139
157
  // src/constants.ts
140
158
  var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
141
- var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
142
- var MAX_IDENTIFIER_LENGTH = 63;
143
159
  var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
144
160
  var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
145
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
161
+ var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
162
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
163
+ var MAX_IDENTIFIER_LENGTH = 63;
164
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
146
165
  var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
147
166
  var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
148
167
  var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
@@ -154,8 +173,31 @@ var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
154
173
  var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
155
174
  var HASH_DOMAIN = "sequence-orm-definitions\0";
156
175
  var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
176
+ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
177
+ var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
157
178
  var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
158
179
  var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
180
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
181
+ var CELL_PART_SUFFIX = {
182
+ at: "__at",
183
+ by: "__by",
184
+ rev: "__rev",
185
+ source: "__src",
186
+ conflicts: "__lost"
187
+ };
188
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
189
+ var CELLS_FIELD = "cells";
190
+ function cellColumnName(column, part) {
191
+ return `${column}${CELL_PART_SUFFIX[part]}`;
192
+ }
193
+ var MATERIALIZE_ACTOR = "orm:materialize";
194
+ var MATERIALIZE_CLAIMS_JSON = JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
195
+ function materializedRefreshFunctionName(table) {
196
+ return `${table}__refresh`;
197
+ }
198
+ function materializeLockKey(schema, table) {
199
+ return `orm:materialize:${schema}.${table}`;
200
+ }
159
201
 
160
202
  // src/errors.ts
161
203
  var OrmDefinitionError = class extends Error {
@@ -231,6 +273,8 @@ function toOne(target, options) {
231
273
  on: options.on,
232
274
  references: options.references,
233
275
  enforce: options.enforce ?? false,
276
+ onDelete: options.onDelete,
277
+ onUpdate: options.onUpdate,
234
278
  description: options.description
235
279
  };
236
280
  }
@@ -242,6 +286,8 @@ function toMany(target, options) {
242
286
  on: options.on,
243
287
  references: options.references,
244
288
  enforce: false,
289
+ onDelete: void 0,
290
+ onUpdate: void 0,
245
291
  description: options.description
246
292
  };
247
293
  }
@@ -253,6 +299,8 @@ function toPlatformUser(options) {
253
299
  on: options.on,
254
300
  references: options.references,
255
301
  enforce: false,
302
+ onDelete: void 0,
303
+ onUpdate: void 0,
256
304
  description: options.description
257
305
  };
258
306
  }
@@ -262,10 +310,10 @@ function isSeqLink(value) {
262
310
  function isSeqTable(value) {
263
311
  return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
264
312
  }
265
- function assertIdentifier({ value, what, file }) {
266
- if (!IDENTIFIER_PATTERN.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
313
+ function assertIdentifier({ value, what, file, pattern = IDENTIFIER_PATTERN }) {
314
+ if (!pattern.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
267
315
  throw new OrmDefinitionError(
268
- `${what} "${value}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
316
+ `${what} "${value}" must match ${pattern} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
269
317
  file
270
318
  );
271
319
  }
@@ -286,7 +334,13 @@ function defineTable(name, columns, config = {}) {
286
334
  );
287
335
  }
288
336
  const columnName = builder.columnNameFor(propertyName);
289
- assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file });
337
+ assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
338
+ if (config.materializes === void 0 && (propertyName === CELLS_FIELD || Object.values(CELL_PART_SUFFIX).some((suffix) => columnName.endsWith(suffix) || propertyName.endsWith(suffix)))) {
339
+ throw new OrmDefinitionError(
340
+ `table "${name}": "${propertyName}" collides with the reserved cell-metadata names ('${CELLS_FIELD}', or a ${Object.values(CELL_PART_SUFFIX).join("/")} suffix)`,
341
+ file
342
+ );
343
+ }
290
344
  const existing = seenColumnNames.get(columnName);
291
345
  if (existing !== void 0) {
292
346
  throw new OrmDefinitionError(
@@ -326,7 +380,8 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
326
380
  assertIdentifier({
327
381
  value: columnName,
328
382
  what: `synced table "${name}": column "${propertyName}" column name`,
329
- file
383
+ file,
384
+ pattern: COLUMN_IDENTIFIER_PATTERN
330
385
  });
331
386
  const existing = seenColumnNames.get(columnName);
332
387
  if (existing !== void 0) {
@@ -415,7 +470,7 @@ function defineView(name, columns, config) {
415
470
  );
416
471
  }
417
472
  const columnName = builder.columnNameFor(propertyName);
418
- if (!IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
473
+ if (!COLUMN_IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
419
474
  throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
420
475
  }
421
476
  const existing = seenColumnNames.get(columnName);
@@ -533,11 +588,11 @@ function compileViewQueries(input) {
533
588
  );
534
589
  }
535
590
  if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
536
- const tableDependencies = extractUsedTable(config.query).map((physical) => {
537
- const logical = physicalToLogical.get(physical);
591
+ const tableDependencies = extractUsedTable(config.query).map((physical2) => {
592
+ const logical = physicalToLogical.get(physical2);
538
593
  if (!logical) {
539
594
  throw new OrmDefinitionError(
540
- `view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
595
+ `view "${authored.viewName}" query references unmanaged relation "${physical2}" \u2014 use relation(...) with a namespace table or view`,
541
596
  authored.file
542
597
  );
543
598
  }
@@ -861,9 +916,478 @@ function scalarColumnType(table, property, helper) {
861
916
  }
862
917
  return type;
863
918
  }
919
+
920
+ // src/compiler/sql.ts
921
+ var CLAIMS_SETTING = "request.jwt.claims";
922
+ var CAPABILITIES_CLAIM_KEY = "capabilities";
923
+ var ROLES_CLAIM_KEY = "roles";
924
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
925
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
926
+ var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
927
+ var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
928
+ var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
929
+ var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
930
+
931
+ // src/checks/compile.ts
932
+ function tokenize(source) {
933
+ const tokens = [];
934
+ for (let position = 0; position < source.length; ) {
935
+ const char = source[position];
936
+ if (/\s/.test(char)) {
937
+ position++;
938
+ continue;
939
+ }
940
+ if (char === "(" || char === ")" || char === ",") {
941
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
942
+ position++;
943
+ continue;
944
+ }
945
+ if (char === "'") {
946
+ const start = position++;
947
+ let value = "";
948
+ let closed = false;
949
+ while (position < source.length) {
950
+ if (source[position] !== "'") {
951
+ value += source[position++];
952
+ continue;
953
+ }
954
+ if (source[position + 1] === "'") {
955
+ value += "'";
956
+ position += 2;
957
+ continue;
958
+ }
959
+ position++;
960
+ closed = true;
961
+ break;
962
+ }
963
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
964
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
965
+ tokens.push({ kind: "string", value, position: start });
966
+ continue;
967
+ }
968
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
969
+ if (operator) {
970
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
971
+ position += operator[0].length;
972
+ continue;
973
+ }
974
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
975
+ if (number) {
976
+ tokens.push({ kind: "number", value: number[0], position });
977
+ position += number[0].length;
978
+ continue;
979
+ }
980
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
981
+ if (identifier3) {
982
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
983
+ position += identifier3[0].length;
984
+ continue;
985
+ }
986
+ throw new Error(`unexpected character '${char}' at position ${position}`);
987
+ }
988
+ tokens.push({ kind: "end", value: "", position: source.length });
989
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
990
+ return tokens;
991
+ }
992
+ var Parser = class {
993
+ constructor(tokens, columns) {
994
+ this.tokens = tokens;
995
+ this.columns = /* @__PURE__ */ new Map();
996
+ for (const column of columns) {
997
+ for (const alias of [column.name, column.columnName]) {
998
+ const previous = this.columns.get(alias);
999
+ if (previous && previous.columnName !== column.columnName) {
1000
+ throw new Error(`ambiguous check column '${alias}'`);
1001
+ }
1002
+ this.columns.set(alias, column);
1003
+ }
1004
+ }
1005
+ }
1006
+ index = 0;
1007
+ depth = 0;
1008
+ columns;
1009
+ parse() {
1010
+ const expression = this.parseOr();
1011
+ this.expect("end", "end of expression");
1012
+ return expression;
1013
+ }
1014
+ parseOr() {
1015
+ let left = this.parseAnd();
1016
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
1017
+ return left;
1018
+ }
1019
+ parseAnd() {
1020
+ let left = this.parseNot();
1021
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
1022
+ return left;
1023
+ }
1024
+ parseNot() {
1025
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
1026
+ return this.parsePrimary();
1027
+ }
1028
+ parsePrimary() {
1029
+ if (this.peek().kind === "lparen") {
1030
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
1031
+ this.advance();
1032
+ const expression = this.parseOr();
1033
+ this.expect("rparen", "')'");
1034
+ this.depth--;
1035
+ return `(${expression})`;
1036
+ }
1037
+ return this.parseComparison();
1038
+ }
1039
+ parseComparison() {
1040
+ const left = this.parseValue();
1041
+ if (this.matchKeyword("is")) {
1042
+ const negate = this.matchKeyword("not");
1043
+ this.expectKeyword("null");
1044
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
1045
+ }
1046
+ if (this.matchKeyword("between")) {
1047
+ const lower = this.parseValue();
1048
+ this.expectKeyword("and");
1049
+ const upper = this.parseValue();
1050
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
1051
+ }
1052
+ if (this.matchKeyword("in")) {
1053
+ this.expect("lparen", "'(' after IN");
1054
+ const values = [this.parseValue()];
1055
+ while (this.peek().kind === "comma") {
1056
+ this.advance();
1057
+ values.push(this.parseValue());
1058
+ }
1059
+ this.expect("rparen", "')' after IN values");
1060
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
1061
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
1062
+ }
1063
+ const operator = this.expect("operator", "a comparison operator");
1064
+ const right = this.parseValue();
1065
+ if (operator.value === "~" || operator.value === "!~") {
1066
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
1067
+ if (!right.literal) throw new Error("regex pattern must be a literal");
1068
+ }
1069
+ return `${left.sql} ${operator.value} ${right.sql}`;
1070
+ }
1071
+ parseValue() {
1072
+ const token = this.advance();
1073
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
1074
+ if (token.kind === "number") {
1075
+ const number = Number(token.value);
1076
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
1077
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
1078
+ }
1079
+ return { sql: token.value, literal: true };
1080
+ }
1081
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
1082
+ const keyword = token.value.toLowerCase();
1083
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
1084
+ if (keyword === "btrim") {
1085
+ this.expect("lparen", "'(' after btrim");
1086
+ const column2 = this.parseColumn();
1087
+ this.expect("rparen", "')' after btrim column");
1088
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
1089
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
1090
+ }
1091
+ const column = this.columns.get(token.value);
1092
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
1093
+ return { sql: quoteIdentifier(column.columnName), column };
1094
+ }
1095
+ parseColumn() {
1096
+ const token = this.expect("identifier", "a column name");
1097
+ const column = this.columns.get(token.value);
1098
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
1099
+ return column;
1100
+ }
1101
+ peek() {
1102
+ return this.tokens[this.index];
1103
+ }
1104
+ advance() {
1105
+ const token = this.peek();
1106
+ if (token.kind !== "end") this.index++;
1107
+ return token;
1108
+ }
1109
+ matchKeyword(keyword) {
1110
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
1111
+ this.advance();
1112
+ return true;
1113
+ }
1114
+ expectKeyword(keyword) {
1115
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
1116
+ }
1117
+ expect(kind, description) {
1118
+ const token = this.peek();
1119
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
1120
+ return this.advance();
1121
+ }
1122
+ };
1123
+ function compileCheckExpression(source, columns) {
1124
+ return new Parser(tokenize(source), columns).parse();
1125
+ }
1126
+ var policyVariantSchema = z.object({
1127
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
1128
+ when: z.string().min(1).max(1e3),
1129
+ /** Exactly one of: allow-all bypass, or a row predicate. */
1130
+ allow: z.literal(true).optional(),
1131
+ policy: z.string().min(1).max(2e3).optional()
1132
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
1133
+ message: "A variant declares exactly one of allow: true or policy"
1134
+ });
1135
+ var policyExpressionSchema = z.union([
1136
+ z.string().min(1).max(2e3),
1137
+ z.object({
1138
+ default: z.string().min(1).max(2e3),
1139
+ variants: z.array(policyVariantSchema).max(10).default([])
1140
+ })
1141
+ ]);
1142
+ function parseDurationMs(input) {
1143
+ if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
1144
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
1145
+ if (!match) return void 0;
1146
+ const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
1147
+ const value = Math.round(Number(match[1]) * factor);
1148
+ return value > 0 ? value : void 0;
1149
+ }
1150
+ var identifier = z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
1151
+ var durationMs = z.number().int().positive().max(24 * 60 * 60 * 1e3);
1152
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
1153
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
1154
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
1155
+ };
1156
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
1157
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
1158
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
1159
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
1160
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
1161
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
1162
+ };
1163
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
1164
+ var RESERVED_RESOLVED_COLUMN_NAMES = /* @__PURE__ */ new Set([
1165
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
1166
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
1167
+ ]);
1168
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
1169
+ var canonicalFunctionRefSchema = z.object({
1170
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
1171
+ kind: z.enum(["managed", "native"]),
1172
+ id: z.string().min(1).max(200)
1173
+ }).strict();
1174
+ var canonicalFetchShapeSchema = z.union([
1175
+ z.object({ shape: z.literal("single") }).strict(),
1176
+ z.object({ shape: z.literal("batch"), maxKeys: z.number().int().min(2).max(500) }).strict()
1177
+ ]);
1178
+ var canonicalResolvedSourceSchema = z.object({
1179
+ name: identifier,
1180
+ kind: z.enum(["manual", "etl", "prefetch"]),
1181
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
1182
+ table: identifier,
1183
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
1184
+ columns: z.array(z.string().min(1).max(63)).min(1).max(500),
1185
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
1186
+ updatedAt: z.string().min(1).max(63).optional(),
1187
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
1188
+ key: z.string().min(1).max(63).optional(),
1189
+ fetch: canonicalFetchShapeSchema.optional(),
1190
+ function: canonicalFunctionRefSchema.optional(),
1191
+ prefetchDefaults: z.object({
1192
+ maxStalenessMs: durationMs,
1193
+ timeoutMs: durationMs,
1194
+ onError: z.enum(["fallback", "fail"])
1195
+ }).strict().optional(),
1196
+ writeback: z.object({
1197
+ columns: z.array(z.string().min(1).max(63)).min(1).max(500),
1198
+ defaultMode: z.enum(["sync", "async"]),
1199
+ timeoutMs: durationMs,
1200
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
1201
+ clearManualOnConfirm: z.boolean()
1202
+ }).strict().optional()
1203
+ }).strict();
1204
+ var canonicalResolveRuleSchema = z.object({
1205
+ column: z.string().min(1).max(63),
1206
+ comparator: z.enum(COMPARATORS),
1207
+ /** Source names, in tie-break order. */
1208
+ candidates: z.array(identifier).min(1).max(16),
1209
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
1210
+ orderBy: z.string().min(1).max(2e3).optional()
1211
+ }).strict();
1212
+ var canonicalResolvedSchema = z.object({
1213
+ sources: z.array(canonicalResolvedSourceSchema).min(1).max(16),
1214
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
1215
+ resolve: z.array(canonicalResolveRuleSchema).max(500),
1216
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
1217
+ deletedBy: z.array(identifier).max(1).default([]),
1218
+ /**
1219
+ * Materialized: the view reads an indexed internal table (`table`) that the
1220
+ * data plane keeps in step with `compute`, the internal view holding the
1221
+ * resolution query. Omitted when the view computes on read.
1222
+ */
1223
+ materialized: z.object({ table: identifier, compute: identifier }).strict().optional(),
1224
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
1225
+ policies: z.object({
1226
+ write: policyExpressionSchema.optional(),
1227
+ insert: policyExpressionSchema.optional(),
1228
+ update: policyExpressionSchema.optional()
1229
+ }).strict().default({})
1230
+ }).strict();
1231
+ function validateResolvedView({ view, tables, views = [] }) {
1232
+ const resolved = view.resolved;
1233
+ if (!resolved) return [];
1234
+ const errors = [];
1235
+ const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
1236
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
1237
+ if (!pkColumn) {
1238
+ fail("requires exactly one primary-key column");
1239
+ return errors;
1240
+ }
1241
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
1242
+ const scope = {
1243
+ view,
1244
+ resolved,
1245
+ pkColumn,
1246
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
1247
+ sourceByName: /* @__PURE__ */ new Map(),
1248
+ fail
1249
+ };
1250
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
1251
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
1252
+ for (const source of resolved.sources) {
1253
+ if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
1254
+ scope.sourceByName.set(source.name, source);
1255
+ const table = tableByName.get(source.table);
1256
+ if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
1257
+ else validateSource({ scope, source, table });
1258
+ }
1259
+ validateRules(scope);
1260
+ validateViewCells(scope);
1261
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
1262
+ for (const name of resolved.deletedBy) {
1263
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
1264
+ }
1265
+ return errors;
1266
+ }
1267
+ function validateSource({ scope, source, table }) {
1268
+ const { fail } = scope;
1269
+ if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
1270
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
1271
+ fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
1272
+ }
1273
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
1274
+ if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
1275
+ if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
1276
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
1277
+ validateSourceColumns({ scope, source, byName });
1278
+ if (source.kind === "etl") {
1279
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
1280
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
1281
+ fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
1282
+ }
1283
+ }
1284
+ validatePrefetchFields({ scope, source });
1285
+ }
1286
+ function validatePrefetchFields({ scope, source }) {
1287
+ const { fail, pkColumn } = scope;
1288
+ if (source.kind !== "prefetch") {
1289
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
1290
+ fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
1291
+ }
1292
+ return;
1293
+ }
1294
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
1295
+ fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
1296
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
1297
+ fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
1298
+ }
1299
+ for (const column of source.writeback?.columns ?? []) {
1300
+ if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
1301
+ }
1302
+ }
1303
+ function validateSourceColumns({ scope, source, byName }) {
1304
+ const { fail, pkColumn, dataColumns } = scope;
1305
+ for (const column of [pkColumn.name, ...source.columns]) {
1306
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
1307
+ const theirs = byName.get(column);
1308
+ if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
1309
+ else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
1310
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
1311
+ }
1312
+ }
1313
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
1314
+ const ruled = /* @__PURE__ */ new Set();
1315
+ for (const rule of resolved.resolve) {
1316
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
1317
+ fail(`resolve rule targets unknown column '${rule.column}'`);
1318
+ continue;
1319
+ }
1320
+ if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
1321
+ ruled.add(rule.column);
1322
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
1323
+ fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
1324
+ }
1325
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
1326
+ for (const candidate of rule.candidates) {
1327
+ const source = sourceByName.get(candidate);
1328
+ if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
1329
+ else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
1330
+ }
1331
+ }
1332
+ for (const column of dataColumns.keys()) {
1333
+ if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
1334
+ }
1335
+ }
1336
+ var VIEW_CELL_TYPES = {
1337
+ source: "text",
1338
+ at: "timestamptz",
1339
+ by: "text",
1340
+ conflicts: "jsonb"
1341
+ };
1342
+ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
1343
+ const expected = /* @__PURE__ */ new Map();
1344
+ for (const column of dataColumns.values()) {
1345
+ if (column.name === pkColumn.name) continue;
1346
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
1347
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
1348
+ }
1349
+ }
1350
+ for (const column of view.columns) {
1351
+ if (!column.cell) continue;
1352
+ const want = expected.get(column.name);
1353
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
1354
+ fail(`unexpected cell column '${column.name}'`);
1355
+ continue;
1356
+ }
1357
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
1358
+ expected.delete(column.name);
1359
+ }
1360
+ for (const name of expected.keys()) fail(`missing cell column '${name}'`);
1361
+ }
1362
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
1363
+ function validateMaterialized({ scope, tableByName, views }) {
1364
+ const { view, resolved, fail } = scope;
1365
+ const { table: tableName, compute: computeName } = resolved.materialized;
1366
+ const table = tableByName.get(tableName);
1367
+ if (!table || table.internal !== true || table.materializes !== view.view) {
1368
+ fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
1369
+ } else {
1370
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
1371
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
1372
+ }
1373
+ const compute = views.find((candidate) => candidate.view === computeName);
1374
+ if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
1375
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
1376
+ for (const source of resolved.sources) {
1377
+ if (source.kind !== "etl") continue;
1378
+ const read = tableByName.get(source.table)?.policies?.read;
1379
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
1380
+ if (!unconditional) {
1381
+ fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
1382
+ }
1383
+ }
1384
+ }
1385
+
1386
+ // src/canonical/v2.ts
864
1387
  var CANONICAL_FORMAT = 2;
865
1388
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
866
- var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
1389
+ var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
1390
+ var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
867
1391
  var COLUMN_TYPES = [
868
1392
  "uuid",
869
1393
  "text",
@@ -878,59 +1402,71 @@ var COLUMN_TYPES = [
878
1402
  ];
879
1403
  var columnTypeSchema = z.enum(COLUMN_TYPES);
880
1404
  var canonicalEnumSchema = z.object({
881
- name: identifier,
1405
+ name: identifier2,
882
1406
  values: z.array(z.string().min(1).max(63)).min(1).max(100)
883
1407
  });
884
1408
  var canonicalColumnSchema = z.object({
885
1409
  /** Property name in TS (camelCase); columnName is the Postgres name. */
886
1410
  name: z.string().min(1).max(63),
887
- columnName: identifier,
888
- type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
1411
+ columnName: columnIdentifier,
1412
+ type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
889
1413
  nullable: z.boolean().default(true),
890
1414
  unique: z.boolean().default(false),
891
1415
  /** Literal default, or one of the allowlisted SQL functions. */
892
1416
  default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
893
1417
  defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
1418
+ /**
1419
+ * Stamp this column from the database clock on UPDATE (`now()`). The
1420
+ * column stays insertable (defaults fill gaps) but is omitted from
1421
+ * SetInput / settable so callers cannot backdate it.
1422
+ */
1423
+ onUpdateFn: z.enum(["now()"]).optional(),
894
1424
  description: z.string().max(500).optional(),
895
1425
  /** Migration hints — same semantics as v1. */
896
- renamedFrom: identifier.optional(),
1426
+ renamedFrom: columnIdentifier.optional(),
897
1427
  cast: z.string().max(200).optional(),
898
- deprecated: z.boolean().default(false)
1428
+ deprecated: z.boolean().default(false),
1429
+ /**
1430
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
1431
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
1432
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
1433
+ * ordinary columns so their hashes are unchanged.
1434
+ */
1435
+ cell: z.object({ of: z.string().min(1).max(63), part: z.enum(CELL_PARTS) }).strict().optional()
1436
+ }).superRefine((column, ctx) => {
1437
+ if (column.onUpdateFn === void 0) return;
1438
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
1439
+ ctx.addIssue({
1440
+ code: "custom",
1441
+ path: ["onUpdateFn"],
1442
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
1443
+ });
1444
+ }
899
1445
  });
900
1446
  var canonicalLinkSchema = z.object({
901
1447
  name: z.string().min(1).max(63),
902
1448
  kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
903
1449
  /** Target table name (namespace-local). Absent for toPlatformUser. */
904
- target: identifier.optional(),
1450
+ target: identifier2.optional(),
905
1451
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
906
- on: identifier,
1452
+ on: columnIdentifier,
907
1453
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
908
- references: identifier.optional(),
1454
+ references: columnIdentifier.optional(),
909
1455
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
910
1456
  enforce: z.boolean().default(false),
1457
+ onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
1458
+ onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
911
1459
  description: z.string().max(500).optional()
912
1460
  });
913
1461
  var canonicalIndexSchema = z.object({
914
- columns: z.array(identifier).min(1),
1462
+ columns: z.array(columnIdentifier).min(1),
915
1463
  unique: z.boolean().default(false),
916
- name: identifier.optional()
1464
+ name: identifier2.optional()
917
1465
  });
918
- var policyVariantSchema = z.object({
919
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
920
- when: z.string().min(1).max(1e3),
921
- /** Exactly one of: allow-all bypass, or a row predicate. */
922
- allow: z.literal(true).optional(),
923
- policy: z.string().min(1).max(2e3).optional()
924
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
925
- message: "A variant declares exactly one of allow: true or policy"
926
- });
927
- var policyExpressionSchema = z.union([
928
- z.string().min(1).max(2e3),
929
- z.object({
930
- default: z.string().min(1).max(2e3),
931
- variants: z.array(policyVariantSchema).max(10).default([])
932
- })
933
- ]);
1466
+ var canonicalCheckSchema = z.object({
1467
+ name: identifier2,
1468
+ expression: z.string().min(1).max(2e3)
1469
+ }).strict();
934
1470
  var canonicalPoliciesSchema = z.object({
935
1471
  read: policyExpressionSchema.optional(),
936
1472
  write: policyExpressionSchema.optional(),
@@ -1118,7 +1654,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
1118
1654
  "width_bucket"
1119
1655
  ];
1120
1656
  var canonicalActionSchema = z.object({
1121
- name: identifier,
1657
+ name: identifier2,
1122
1658
  requires: z.string().max(1e3).optional(),
1123
1659
  params: z.record(z.string(), z.object({
1124
1660
  type: columnTypeSchema,
@@ -1130,9 +1666,9 @@ var canonicalActionSchema = z.object({
1130
1666
  });
1131
1667
  var canonicalCowMutationSchema = z.object({
1132
1668
  kind: z.enum(["override", "revert"]),
1133
- name: identifier,
1669
+ name: identifier2,
1134
1670
  /** Synced table (same namespace) the overlay copies from / falls back to. */
1135
- synced: identifier,
1671
+ synced: identifier2,
1136
1672
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
1137
1673
  pk: z.string().min(1).max(63),
1138
1674
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -1159,17 +1695,28 @@ var canonicalCowMutationSchema = z.object({
1159
1695
  }
1160
1696
  }
1161
1697
  });
1698
+ var canonicalForeignKeySchema = z.object({
1699
+ name: identifier2,
1700
+ columns: z.array(columnIdentifier).min(1).max(8),
1701
+ target: identifier2,
1702
+ references: z.array(columnIdentifier).min(1).max(8),
1703
+ deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional(),
1704
+ onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
1705
+ onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
1706
+ }).strict();
1162
1707
  var canonicalTableSchema = z.object({
1163
- table: identifier,
1708
+ table: identifier2,
1164
1709
  description: z.string().max(2e3).optional(),
1165
1710
  /** Table-level rename hint (new in v2). */
1166
- renamedFrom: identifier.optional(),
1711
+ renamedFrom: identifier2.optional(),
1167
1712
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
1168
1713
  source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
1169
1714
  columns: z.array(canonicalColumnSchema).default([]),
1170
- primaryKey: z.array(identifier).min(1).max(8).optional(),
1715
+ primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
1171
1716
  links: z.array(canonicalLinkSchema).default([]),
1172
1717
  indexes: z.array(canonicalIndexSchema).default([]),
1718
+ /** Omitted by old definitions, preserving their existing content hashes. */
1719
+ checks: z.array(canonicalCheckSchema).max(100).optional(),
1173
1720
  policies: canonicalPoliciesSchema.default({}),
1174
1721
  /**
1175
1722
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -1178,26 +1725,64 @@ var canonicalTableSchema = z.object({
1178
1725
  */
1179
1726
  actions: z.array(canonicalActionSchema).default([]),
1180
1727
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
1181
- cowMutations: z.array(canonicalCowMutationSchema).optional()
1728
+ cowMutations: z.array(canonicalCowMutationSchema).optional(),
1729
+ /** Omitted by old definitions, preserving their existing content hashes. */
1730
+ foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional(),
1731
+ /**
1732
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
1733
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
1734
+ * on a resolved table's internal sources), listed explicitly in `columns`
1735
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
1736
+ * Omitted when off so existing definitions hash unchanged.
1737
+ */
1738
+ cellMetadata: z.literal(true).optional(),
1739
+ /**
1740
+ * System-owned backing table of a resolved table (its manual or prefetch
1741
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
1742
+ * only the data plane writes them. Omitted when false.
1743
+ */
1744
+ internal: z.literal(true).optional(),
1745
+ /**
1746
+ * The materialized copy of resolved table `materializes` (internal): the
1747
+ * same columns as that view, including its cell parts, maintained by the
1748
+ * data plane. Omitted otherwise.
1749
+ */
1750
+ materializes: identifier2.optional()
1182
1751
  });
1752
+ var MAX_VIEW_QUERY = 1e5;
1753
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
1183
1754
  var canonicalViewSchema = z.object({
1184
- view: identifier,
1755
+ view: identifier2,
1185
1756
  description: z.string().max(2e3).optional(),
1186
1757
  columns: z.array(canonicalColumnSchema).min(1),
1187
1758
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
1188
- primaryKey: z.array(identifier).min(1).max(8).optional(),
1759
+ primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
1189
1760
  links: z.array(canonicalLinkSchema).default([]),
1190
1761
  policies: z.object({ read: policyExpressionSchema }).strict(),
1191
- query: z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
1762
+ query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
1192
1763
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
1193
1764
  }),
1194
1765
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
1195
- dependencies: z.array(identifier).max(500)
1766
+ dependencies: z.array(identifier2).max(500),
1767
+ /**
1768
+ * Present when this view is a resolved table (defineResolvedTable): the
1769
+ * multi-source metadata behind its generated query. Omitted otherwise so
1770
+ * existing views hash unchanged.
1771
+ */
1772
+ resolved: canonicalResolvedSchema.optional(),
1773
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
1774
+ internal: z.literal(true).optional()
1196
1775
  }).superRefine((view, ctx) => {
1776
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
1777
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
1778
+ }
1197
1779
  for (const [index3, column] of view.columns.entries()) {
1198
1780
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1199
1781
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
1200
1782
  }
1783
+ if (column.onUpdateFn !== void 0) {
1784
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare onUpdateFn` });
1785
+ }
1201
1786
  if (column.unique) {
1202
1787
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
1203
1788
  }
@@ -1205,15 +1790,82 @@ var canonicalViewSchema = z.object({
1205
1790
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
1206
1791
  }
1207
1792
  }
1793
+ if (view.resolved === void 0 && view.internal !== true) {
1794
+ for (const [index3, column] of view.columns.entries()) {
1795
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index3], message: `only resolved tables carry cell columns ('${column.name}')` });
1796
+ }
1797
+ }
1208
1798
  for (const [index3, link] of view.links.entries()) {
1209
1799
  if (link.enforce) {
1210
1800
  ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
1211
1801
  }
1802
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
1803
+ ctx.addIssue({ code: "custom", path: ["links", index3], message: `view link '${link.name}' cannot declare referential actions` });
1804
+ }
1212
1805
  }
1213
1806
  });
1807
+ var CELL_PART_TYPES = {
1808
+ at: "timestamptz",
1809
+ by: "text",
1810
+ rev: "uuid",
1811
+ source: "text",
1812
+ conflicts: "jsonb"
1813
+ };
1814
+ function trackedCellColumns(table) {
1815
+ const pk = new Set(table.primaryKey ?? []);
1816
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
1817
+ }
1818
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
1819
+ function validateCellMetadata(table, index3, ctx) {
1820
+ const fail = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
1821
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
1822
+ if (table.materializes !== void 0) return;
1823
+ if (table.cellMetadata !== true) {
1824
+ for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index3, "columns", position]);
1825
+ return;
1826
+ }
1827
+ if (table.source !== void 0) {
1828
+ fail(`synced table '${table.table}' cannot track cell metadata`);
1829
+ return;
1830
+ }
1831
+ const tracked = trackedCellColumns(table);
1832
+ const byName = new Map(tracked.map((column) => [column.name, column]));
1833
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
1834
+ if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
1835
+ const seen = /* @__PURE__ */ new Set();
1836
+ for (const { column, position, cell } of cells) {
1837
+ const path = ["tables", index3, "columns", position];
1838
+ const owner = byName.get(cell.of);
1839
+ if (!owner) {
1840
+ fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
1841
+ continue;
1842
+ }
1843
+ if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
1844
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
1845
+ fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
1846
+ }
1847
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
1848
+ fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
1849
+ }
1850
+ const key = `${cell.of}:${cell.part}`;
1851
+ if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
1852
+ seen.add(key);
1853
+ }
1854
+ for (const column of tracked) {
1855
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
1856
+ fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
1857
+ }
1858
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
1859
+ fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
1860
+ }
1861
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
1862
+ if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
1863
+ }
1864
+ }
1865
+ }
1214
1866
  var canonicalDefinitionSchema = z.object({
1215
1867
  definition_format: z.literal(CANONICAL_FORMAT),
1216
- namespace: identifier.max(41),
1868
+ namespace: identifier2.max(41),
1217
1869
  description: z.string().max(2e3).optional(),
1218
1870
  enums: z.array(canonicalEnumSchema).default([]),
1219
1871
  tables: z.array(canonicalTableSchema).min(0).max(500),
@@ -1231,6 +1883,74 @@ var canonicalDefinitionSchema = z.object({
1231
1883
  ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
1232
1884
  }
1233
1885
  seen.add(table.table);
1886
+ validateCellMetadata(table, index3, ctx);
1887
+ if (table.source !== void 0) {
1888
+ for (const [columnIndex, column] of table.columns.entries()) {
1889
+ if (column.onUpdateFn !== void 0) {
1890
+ ctx.addIssue({
1891
+ code: "custom",
1892
+ path: ["tables", index3, "columns", columnIndex],
1893
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
1894
+ });
1895
+ }
1896
+ }
1897
+ }
1898
+ }
1899
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
1900
+ for (const [tableIndex, table] of definition.tables.entries()) {
1901
+ const names = /* @__PURE__ */ new Set([
1902
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
1903
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
1904
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
1905
+ ]);
1906
+ for (const [index3, link] of table.links.entries()) {
1907
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
1908
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "referential actions require an enforced toOne link" });
1909
+ }
1910
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
1911
+ const source = table.columns.find((column) => column.columnName === link.on);
1912
+ if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "SET NULL requires a nullable source column" });
1913
+ }
1914
+ }
1915
+ for (const [index3, check] of (table.checks ?? []).entries()) {
1916
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index3], message });
1917
+ if (table.source) fail("synced tables cannot declare checks");
1918
+ if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
1919
+ names.add(check.name);
1920
+ try {
1921
+ compileCheckExpression(check.expression, table.columns);
1922
+ } catch (error) {
1923
+ fail(error instanceof Error ? error.message : String(error));
1924
+ }
1925
+ }
1926
+ for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
1927
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
1928
+ if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
1929
+ names.add(fk.name);
1930
+ const target = fkTables.get(fk.target);
1931
+ if (table.source || !target || target.source) {
1932
+ fail("foreign keys must connect owned tables in the same namespace");
1933
+ continue;
1934
+ }
1935
+ if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
1936
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
1937
+ for (const [position, name] of fk.columns.entries()) {
1938
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
1939
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
1940
+ if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
1941
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
1942
+ }
1943
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
1944
+ if (fk.columns.some((name) => table.columns.find((column) => column.columnName === name)?.nullable === false || table.primaryKey?.includes(name))) fail("SET NULL requires nullable source columns");
1945
+ }
1946
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
1947
+ if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
1948
+ }
1949
+ }
1950
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
1951
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
1952
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
1953
+ }
1234
1954
  }
1235
1955
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
1236
1956
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -1355,8 +2075,12 @@ function canonicalRelations(definition) {
1355
2075
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
1356
2076
  links: view.links,
1357
2077
  indexes: [],
1358
- policies: { read: view.policies.read },
1359
- actions: []
2078
+ // A resolved table's write policies (evaluated against the resolved row)
2079
+ // ride alongside the view's read policy.
2080
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
2081
+ actions: [],
2082
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
2083
+ ...view.internal ? { internal: true } : {}
1360
2084
  }));
1361
2085
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
1362
2086
  }
@@ -1366,12 +2090,14 @@ function normalizeCanonical(definition) {
1366
2090
  ...definition,
1367
2091
  enums: sortByName(definition.enums),
1368
2092
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
1369
- const { cowMutations, ...rest } = t2;
2093
+ const { cowMutations, foreignKeys, checks, ...rest } = t2;
1370
2094
  return {
1371
2095
  ...rest,
1372
2096
  links: sortByName(t2.links),
1373
2097
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
1374
2098
  actions: sortByName(t2.actions),
2099
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
2100
+ ...checks?.length ? { checks: sortByName(checks) } : {},
1375
2101
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
1376
2102
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
1377
2103
  };
@@ -1386,6 +2112,454 @@ function normalizeCanonical(definition) {
1386
2112
  }
1387
2113
  return normalized;
1388
2114
  }
2115
+
2116
+ // src/authoring/resolved-sources.ts
2117
+ function cloneColumn({ builder, property, notNull, primaryKey: primaryKey2 }) {
2118
+ const canonical = builder.toCanonicalColumn(property);
2119
+ let clone = new ColumnBuilder(canonical.type, canonical.columnName);
2120
+ if (primaryKey2) clone = clone.primaryKey();
2121
+ if (notNull) clone = clone.notNull();
2122
+ if (canonical.description !== void 0) clone = clone.description(canonical.description);
2123
+ return clone;
2124
+ }
2125
+ function planSource(input) {
2126
+ const { sourceName, input: source, fail } = input;
2127
+ if (!IDENTIFIER_PATTERN.test(sourceName)) fail(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
2128
+ switch (source.kind) {
2129
+ case "manual":
2130
+ return {
2131
+ name: sourceName,
2132
+ kind: "manual",
2133
+ columns: input.dataColumns,
2134
+ table: internalTable(input, input.dataColumns, {
2135
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned(),
2136
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName),
2137
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName),
2138
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.columnName)
2139
+ }, { revisions: true })
2140
+ };
2141
+ case "etl":
2142
+ return planEtl(input, source);
2143
+ case "prefetch":
2144
+ return planPrefetch(input, source);
2145
+ }
2146
+ }
2147
+ var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
2148
+ function internalTable(input, supplied, system, options = {}) {
2149
+ const { viewName, sourceName, columns, pk, pkBuilder } = input;
2150
+ const tableColumns = { [pk]: cloneColumn({ builder: pkBuilder, property: pk, notNull: true, primaryKey: true }) };
2151
+ for (const property of supplied) {
2152
+ tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
2153
+ }
2154
+ return defineTable(`${viewName}__${sourceName}`, { ...tableColumns, ...system }, {
2155
+ description: `Internal ${input.input.kind} source "${sourceName}" of resolved table "${viewName}". System-maintained.`,
2156
+ cellMetadata: true,
2157
+ // The manual source identifies each edit (`c__rev`) so an async
2158
+ // confirmation clears only the exact edit it delivered.
2159
+ ...options.revisions ? { cellRevisions: true } : {},
2160
+ internal: true
2161
+ });
2162
+ }
2163
+ function assertColumns(input, list, what) {
2164
+ for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
2165
+ }
2166
+ function planEtl(input, source) {
2167
+ const { sourceName, pk, dataColumns, fail } = input;
2168
+ const table = typeof source.table === "function" ? source.table() : source.table;
2169
+ if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
2170
+ const synced = table;
2171
+ if (!(pk in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
2172
+ const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
2173
+ assertColumns(input, supplied, `etl source "${sourceName}"`);
2174
+ if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
2175
+ fail(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
2176
+ }
2177
+ return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
2178
+ }
2179
+ function planPrefetch(input, source) {
2180
+ const { sourceName, pk, fail } = input;
2181
+ assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
2182
+ if (source.key !== pk && !source.columns.includes(source.key)) {
2183
+ fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
2184
+ }
2185
+ for (const column of source.writeback?.columns ?? []) {
2186
+ if (!source.columns.includes(column)) fail(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
2187
+ }
2188
+ const writeback = source.writeback;
2189
+ return {
2190
+ name: sourceName,
2191
+ kind: "prefetch",
2192
+ columns: [...source.columns],
2193
+ key: source.key,
2194
+ function: source.function,
2195
+ fetch: source.fetch === "single" ? { shape: "single" } : { shape: "batch", maxKeys: source.fetch.batch.maxKeys },
2196
+ prefetchDefaults: {
2197
+ maxStalenessMs: parseDuration(source.prefetchDefaults?.maxStaleness ?? "30s"),
2198
+ timeoutMs: parseDuration(source.prefetchDefaults?.timeout ?? "1500ms"),
2199
+ onError: source.prefetchDefaults?.onError ?? "fallback"
2200
+ },
2201
+ ...writeback ? {
2202
+ writeback: {
2203
+ columns: [...writeback.columns],
2204
+ defaultMode: writeback.defaultMode ?? "sync",
2205
+ timeoutMs: parseDuration(writeback.timeout ?? "5s"),
2206
+ clearManualOnConfirm: writeback.clearManualOnConfirm ?? true
2207
+ }
2208
+ } : {},
2209
+ table: internalTable(input, source.columns, {
2210
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.columnName).notNull().default("now()"),
2211
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned()
2212
+ })
2213
+ };
2214
+ }
2215
+ function parseDuration(input) {
2216
+ const value = parseDurationMs(input);
2217
+ if (value === void 0) throw new OrmDefinitionError(`invalid duration "${input}" \u2014 use e.g. '1500ms', '30s', '5m'`);
2218
+ return value;
2219
+ }
2220
+ var PG_TYPE = {
2221
+ uuid: "uuid",
2222
+ text: "text",
2223
+ integer: "integer",
2224
+ bigint: "bigint",
2225
+ numeric: "numeric",
2226
+ double: "double precision",
2227
+ boolean: "boolean",
2228
+ timestamptz: "timestamptz",
2229
+ date: "date",
2230
+ jsonb: "jsonb"
2231
+ };
2232
+ var ident = (value) => `"${value.replace(/"/g, '""')}"`;
2233
+ var literal = (value) => `'${value.replace(/'/g, "''")}'`;
2234
+ var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
2235
+ function candidateSql({ source, column, pk, pgType }) {
2236
+ const a = ident(`s_${source.name}`);
2237
+ const name = physical({ source, property: column });
2238
+ const value = `CAST(${a}.${ident(name)} AS ${pgType})`;
2239
+ if (source.kind === "etl") {
2240
+ return {
2241
+ source: source.name,
2242
+ value,
2243
+ has: `(${a}.${ident(physical({ source, property: pk }))} IS NOT NULL)`,
2244
+ at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
2245
+ by: "CAST(NULL AS text)"
2246
+ };
2247
+ }
2248
+ const at = `${a}.${ident(cellColumnName(name, "at"))}`;
2249
+ const tombstoned2 = source.kind === "prefetch" ? ` AND NOT ${a}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName)}` : "";
2250
+ return { source: source.name, value, has: `(${at} IS NOT NULL${tombstoned2})`, at, by: `${a}.${ident(cellColumnName(name, "by"))}` };
2251
+ }
2252
+ function outranks({ i, j, key, direction, inclusive }) {
2253
+ const ki = i[key];
2254
+ const kj = j[key];
2255
+ const op = inclusive ? `${direction}=` : direction;
2256
+ return inclusive ? `(${kj} IS NULL OR (${ki} IS NOT NULL AND ${ki} ${op} ${kj}))` : `(${ki} IS NOT NULL AND (${kj} IS NULL OR ${ki} ${op} ${kj}))`;
2257
+ }
2258
+ function rankedWinner({ candidates, key, direction }) {
2259
+ const branches = candidates.map((candidate, index3) => {
2260
+ const beats = candidates.map((other, otherIndex) => otherIndex === index3 ? null : `(NOT ${other.has} OR ${outranks({ i: candidate, j: other, key, direction, inclusive: index3 < otherIndex })})`).filter((clause) => clause !== null);
2261
+ return `WHEN ${[candidate.has, ...beats].join(" AND ")} THEN ${index3 + 1}`;
2262
+ });
2263
+ return `CASE ${branches.join(" ")} END`;
2264
+ }
2265
+ function winnerIndex({ comparator, candidates }) {
2266
+ const firstWith = (test) => candidates.map((candidate, index3) => `WHEN ${test(candidate)} THEN ${index3 + 1}`).join(" ");
2267
+ if (candidates.length === 1 || comparator === "priority") return `CASE ${firstWith((c) => c.has)} END`;
2268
+ switch (comparator) {
2269
+ case "latest":
2270
+ return rankedWinner({ candidates, key: "at", direction: ">" });
2271
+ case "highest":
2272
+ return rankedWinner({ candidates, key: "value", direction: ">" });
2273
+ case "lowest":
2274
+ return rankedWinner({ candidates, key: "value", direction: "<" });
2275
+ case "nonNullFirst":
2276
+ return `CASE ${firstWith((c) => `${c.has} AND ${c.value} IS NOT NULL`)} ${firstWith((c) => c.has)} END`;
2277
+ }
2278
+ }
2279
+ function pick(winner, candidates, field) {
2280
+ return `CASE ${winner} ${candidates.map((candidate, index3) => `WHEN ${index3 + 1} THEN ${field(candidate)}`).join(" ")} END`;
2281
+ }
2282
+ function loserSql({ loser, winner }) {
2283
+ return `CASE WHEN ${loser.has} AND ${loser.value} IS DISTINCT FROM ${winner.value} AND ${loser.at} > ${winner.at} THEN jsonb_build_array(jsonb_build_object('source', ${literal(loser.source)}, 'value', to_jsonb(${loser.value}), 'at', ${loser.at})) ELSE '[]'::jsonb END`;
2284
+ }
2285
+ function losersAgainst({ candidates, winner, isWinner }) {
2286
+ const losers = candidates.flatMap((loser, index3) => {
2287
+ const excluded = isWinner(loser, index3);
2288
+ if (excluded === true) return [];
2289
+ const clause = loserSql({ loser, winner });
2290
+ return [excluded === false ? clause : `CASE WHEN ${excluded} THEN '[]'::jsonb ELSE ${clause} END`];
2291
+ });
2292
+ return losers.length === 0 ? "CAST(NULL AS jsonb)" : `NULLIF(${losers.join(" || ")}, '[]'::jsonb)`;
2293
+ }
2294
+ function builtinCell({ comparator, candidates }) {
2295
+ const winner = winnerIndex({ comparator, candidates });
2296
+ const conflicts = comparator === "latest" || candidates.length < 2 ? "CAST(NULL AS jsonb)" : `CASE ${winner} ${candidates.map((known, index3) => `WHEN ${index3 + 1} THEN ${losersAgainst({ candidates, winner: known, isWinner: (_c, other) => other === index3 })}`).join(" ")} END`;
2297
+ return {
2298
+ value: pick(winner, candidates, (c) => c.value),
2299
+ parts: {
2300
+ source: pick(winner, candidates, (c) => literal(c.source)),
2301
+ at: pick(winner, candidates, (c) => c.at),
2302
+ by: pick(winner, candidates, (c) => c.by),
2303
+ conflicts
2304
+ }
2305
+ };
2306
+ }
2307
+ function customCell({ candidates, orderBy, index: index3 }) {
2308
+ const w = ident(`w_${index3}`);
2309
+ const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
2310
+ const lateral = ` LEFT JOIN LATERAL (SELECT max(winner.source) AS source, (array_agg(winner.value))[1] AS value, max(winner.updated_at) AS updated_at, max(winner.actor) AS actor FROM (WITH cand(source, has, value, updated_at, actor, ord) AS (VALUES ${rows.join(", ")}) SELECT c.source, c.value, c.updated_at, c.actor FROM cand c WHERE c.has ORDER BY ${orderBy}, c.ord LIMIT 1) AS winner) AS ${w} ON true`;
2311
+ const winner = { value: `${w}.value`, at: `${w}.updated_at` };
2312
+ return {
2313
+ value: winner.value,
2314
+ lateral,
2315
+ parts: {
2316
+ source: `${w}.source`,
2317
+ at: winner.at,
2318
+ by: `${w}.actor`,
2319
+ conflicts: candidates.length < 2 ? "CAST(NULL AS jsonb)" : losersAgainst({ candidates, winner, isWinner: (c) => `${w}.source = ${literal(c.source)}` })
2320
+ }
2321
+ };
2322
+ }
2323
+ function buildResolvedQuery({ context, plan }) {
2324
+ const { qb, relation } = context;
2325
+ const pkColumn = plan.columns[plan.pk].columnNameFor(plan.pk);
2326
+ const alias = (source) => ident(`s_${source}`);
2327
+ const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
2328
+ const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
2329
+ const schema = getTableConfig(relation(plan.plans[0].table)).schema;
2330
+ const keyBranches = plan.plans.map((source) => {
2331
+ const where = source.kind === "prefetch" ? sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : sql.raw("");
2332
+ const sourcePk = ident(physical({ source, property: plan.pk }));
2333
+ const selectedPk = sourcePk === ident(pkColumn) ? sourcePk : `${sourcePk} AS ${ident(pkColumn)}`;
2334
+ return sql`SELECT ${sql.raw(selectedPk)} FROM ${relation(source.table)}${where}`;
2335
+ });
2336
+ const joins = plan.plans.map(
2337
+ (source) => sql` LEFT JOIN ${relation(source.table)} AS ${sql.raw(alias(source.name))} ON ${sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
2338
+ );
2339
+ const cells = /* @__PURE__ */ new Map();
2340
+ plan.rules.forEach((rule, index3) => {
2341
+ const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
2342
+ const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
2343
+ const candidates = rule.candidates.map((name) => candidateSql({ source: sourceByName.get(name), column: rule.column, pk: plan.pk, pgType }));
2344
+ cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index: index3 }) : builtinCell({ comparator: rule.comparator, candidates }));
2345
+ });
2346
+ const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [sql.raw(cell.lateral)] : []);
2347
+ const from = sql.join([sql`(${sql.join(keyBranches, sql.raw(" UNION "))}) AS "k"`, ...joins, ...laterals], sql.raw(""));
2348
+ const selected = {
2349
+ [plan.pk]: sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
2350
+ };
2351
+ for (const [property, builder] of Object.entries(plan.columns)) {
2352
+ if (property === plan.pk) continue;
2353
+ const cell = cells.get(property);
2354
+ const columnName = builder.columnNameFor(property);
2355
+ selected[property] = sql.raw(cell.value).as(columnName);
2356
+ for (const [part, expression] of Object.entries(cell.parts)) {
2357
+ selected[cellColumnName(property, part)] = sql.raw(expression).as(cellColumnName(columnName, part));
2358
+ }
2359
+ }
2360
+ const manual = plan.plans.find((source) => source.kind === "manual");
2361
+ const system = RESOLVED_VIEW_SYSTEM_COLUMNS;
2362
+ selected[system.writebackStatus.name] = sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName)}`).as(system.writebackStatus.columnName);
2363
+ selected[system.writebackError.name] = sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName)}`).as(system.writebackError.columnName);
2364
+ const hidden = plan.deletedBy.map((name) => `NOT COALESCE(${alias(name)}.${ident(tombstoned2)}, false)`);
2365
+ const query = qb.select(selected).from(from);
2366
+ return hidden.length > 0 ? query.where(sql.raw(hidden.join(" AND "))) : query;
2367
+ }
2368
+ function managedFn(id) {
2369
+ return { kind: "managed", id };
2370
+ }
2371
+ function manualSource() {
2372
+ return { kind: "manual" };
2373
+ }
2374
+ function etlSource(input) {
2375
+ return { kind: "etl", ...input };
2376
+ }
2377
+ function prefetchSource(input) {
2378
+ return { kind: "prefetch", ...input };
2379
+ }
2380
+ var latest = (candidates) => ({ kind: "latest", candidates });
2381
+ var highest = (candidates) => ({ kind: "highest", candidates });
2382
+ var lowest = (candidates) => ({ kind: "lowest", candidates });
2383
+ var priority = (candidates) => ({ kind: "priority", candidates });
2384
+ var nonNullFirst = (candidates) => ({ kind: "nonNullFirst", candidates });
2385
+ var custom = (candidates, orderBy) => ({ kind: "custom", candidates, orderBy });
2386
+ var SYSTEM_VIEW_COLUMNS = {
2387
+ [RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.columnName),
2388
+ [RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.columnName)
2389
+ };
2390
+ function defineResolvedTable(name, columns, config) {
2391
+ const file = activeAuthoringContext()?.currentFile;
2392
+ const fail = (message) => {
2393
+ throw new OrmDefinitionError(`resolved table "${name}": ${message}`, file);
2394
+ };
2395
+ if (!IDENTIFIER_PATTERN.test(name)) fail(`name must match ${IDENTIFIER_PATTERN}`);
2396
+ for (const property of Object.keys(columns)) {
2397
+ if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
2398
+ fail(`column "${property}" collides with a reserved system column`);
2399
+ }
2400
+ }
2401
+ for (const property of Object.keys(columns)) {
2402
+ if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
2403
+ fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
2404
+ }
2405
+ }
2406
+ const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
2407
+ if (pkEntries.length !== 1) fail("declare exactly one .primaryKey() column");
2408
+ const [pk, pkBuilder] = pkEntries[0];
2409
+ const dataColumns = Object.keys(columns).filter((property) => property !== pk);
2410
+ if (dataColumns.length === 0) fail("declare at least one non-key column");
2411
+ const sourceEntries = Object.entries(config.sources);
2412
+ if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
2413
+ const plans = sourceEntries.map(
2414
+ ([sourceName, input]) => planSource({ viewName: name, sourceName, input, columns, pk, pkBuilder, dataColumns, fail })
2415
+ );
2416
+ const rules = planRules({ config, plans, dataColumns, fail });
2417
+ const deletedBy = [...config.rows?.deletedBy ?? []];
2418
+ for (const source of deletedBy) {
2419
+ if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
2420
+ }
2421
+ const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
2422
+ const viewColumns = {};
2423
+ for (const [property, builder] of Object.entries(columns)) {
2424
+ viewColumns[property] = cloneColumn({ builder, property, notNull: property === pk, primaryKey: property === pk });
2425
+ if (property === pk) continue;
2426
+ const columnName = builder.columnNameFor(property);
2427
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
2428
+ viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
2429
+ }
2430
+ }
2431
+ for (const [property, make] of Object.entries(SYSTEM_VIEW_COLUMNS)) viewColumns[property] = make();
2432
+ const { read, ...writePolicies } = config.policies;
2433
+ const resolved = {
2434
+ internalTables: plans.filter((plan) => plan.kind !== "etl").map((plan) => plan.table),
2435
+ sources: plans,
2436
+ rules,
2437
+ deletedBy,
2438
+ policies: writePolicies
2439
+ };
2440
+ const queryPlan = { columns, pk, plans, rules, deletedBy };
2441
+ const typedColumns = viewColumns;
2442
+ const surface = {
2443
+ ...config.description !== void 0 ? { description: config.description } : {},
2444
+ ...config.links !== void 0 ? { links: config.links } : {},
2445
+ policies: { read }
2446
+ };
2447
+ if (!config.materialize) {
2448
+ return defineView(name, typedColumns, { ...surface, query: (context) => buildResolvedQuery({ context, plan: queryPlan }), resolved });
2449
+ }
2450
+ const materialize = config.materialize === true ? {} : config.materialize;
2451
+ const computeName = `${name}__compute`;
2452
+ const tableName = `${name}__materialized`;
2453
+ const compute = defineView(computeName, typedColumns, {
2454
+ policies: { read },
2455
+ query: (context) => buildResolvedQuery({ context, plan: queryPlan }),
2456
+ internal: true
2457
+ });
2458
+ const table = defineTable(tableName, viewColumns, {
2459
+ description: `Materialized rows of resolved table "${name}". System-maintained from "${computeName}".`,
2460
+ internal: true,
2461
+ materializes: name,
2462
+ ...materialize.indexes !== void 0 ? { indexes: materialize.indexes } : {}
2463
+ });
2464
+ return defineView(name, typedColumns, {
2465
+ ...surface,
2466
+ query: (context) => {
2467
+ const handle = context.relation(table);
2468
+ const selected = Object.fromEntries(Object.entries(viewColumns).map(([property, builder]) => [
2469
+ property,
2470
+ sql`${handle[property]}`.as(builder.columnNameFor(property))
2471
+ ]));
2472
+ return context.qb.select(selected).from(context.relation(table));
2473
+ },
2474
+ resolved: {
2475
+ ...resolved,
2476
+ internalTables: [...resolved.internalTables, table],
2477
+ internalViews: [compute],
2478
+ materialized: { table: tableName, compute: computeName }
2479
+ }
2480
+ });
2481
+ }
2482
+ function planRules({ config, plans, dataColumns, fail }) {
2483
+ const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
2484
+ const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
2485
+ for (const column of Object.keys(config.resolve ?? {})) {
2486
+ if (!dataColumns.includes(column)) fail(`resolve names unknown column "${column}"`);
2487
+ }
2488
+ return dataColumns.map((column) => {
2489
+ const explicit = config.resolve?.[column];
2490
+ const comparator = explicit ?? fallback;
2491
+ const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name) => plans.find((plan) => plan.name === name)?.columns.includes(column));
2492
+ for (const candidate of candidates) {
2493
+ const plan = plans.find((entry) => entry.name === candidate);
2494
+ if (!plan) fail(`column "${column}" names unknown source "${candidate}"`);
2495
+ else if (!plan.columns.includes(column)) fail(`column "${column}" candidate "${candidate}" does not supply it`);
2496
+ }
2497
+ if (candidates.length === 0) fail(`column "${column}" has no candidate source`);
2498
+ if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail });
2499
+ return {
2500
+ column,
2501
+ comparator: comparator.kind,
2502
+ candidates,
2503
+ ...comparator.kind === "custom" && comparator.orderBy !== void 0 ? { orderBy: comparator.orderBy } : {}
2504
+ };
2505
+ });
2506
+ }
2507
+ var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
2508
+ "value",
2509
+ "updated_at",
2510
+ "source",
2511
+ "actor",
2512
+ "ord",
2513
+ "asc",
2514
+ "desc",
2515
+ "nulls",
2516
+ "first",
2517
+ "last",
2518
+ "case",
2519
+ "when",
2520
+ "then",
2521
+ "else",
2522
+ "end",
2523
+ "and",
2524
+ "or",
2525
+ "not",
2526
+ "is",
2527
+ "null",
2528
+ "true",
2529
+ "false",
2530
+ "in",
2531
+ "distinct",
2532
+ "from",
2533
+ "coalesce",
2534
+ "nullif",
2535
+ "greatest",
2536
+ "least",
2537
+ "text",
2538
+ "numeric",
2539
+ "integer",
2540
+ "bigint",
2541
+ "boolean",
2542
+ "timestamptz",
2543
+ "date",
2544
+ "jsonb",
2545
+ "interval"
2546
+ ]);
2547
+ var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
2548
+ var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
2549
+ function assertSafeOrderBy({ orderBy, column, fail }) {
2550
+ ORDER_BY_TOKEN.lastIndex = 0;
2551
+ let consumed = 0;
2552
+ for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
2553
+ consumed = ORDER_BY_TOKEN.lastIndex;
2554
+ const token = match[0].toLowerCase();
2555
+ if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
2556
+ fail(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
2557
+ }
2558
+ }
2559
+ if (consumed !== orderBy.length || orderBy.trim() === "") {
2560
+ fail(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
2561
+ }
2562
+ }
1389
2563
  function stableJson(value) {
1390
2564
  if (value === null || typeof value !== "object") return JSON.stringify(value);
1391
2565
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -1492,8 +2666,19 @@ function defineNamespace(input) {
1492
2666
  return config;
1493
2667
  }
1494
2668
  function serializeNamespace(input) {
1495
- const { config, tables } = input;
1496
- const views = input.views ?? [];
2669
+ const { config } = input;
2670
+ const views = [...input.views ?? []];
2671
+ const tables = [...input.tables];
2672
+ for (const view of views) {
2673
+ for (const internal of view.config.resolved?.internalTables ?? []) {
2674
+ if (!tables.includes(internal)) tables.push(internal);
2675
+ }
2676
+ }
2677
+ for (const view of [...views]) {
2678
+ for (const internal of view.config.resolved?.internalViews ?? []) {
2679
+ if (!views.includes(internal)) views.push(internal);
2680
+ }
2681
+ }
1497
2682
  const enums = input.enums ?? [];
1498
2683
  const tablesByName = /* @__PURE__ */ new Map();
1499
2684
  for (const table of tables) {
@@ -1595,6 +2780,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1595
2780
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
1596
2781
  if (table.source !== void 0) candidate.source = table.source;
1597
2782
  if (table.source !== void 0) {
2783
+ if (table.config.checks?.length) {
2784
+ throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
2785
+ }
1598
2786
  if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
1599
2787
  throw new OrmDefinitionError(
1600
2788
  `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
@@ -1632,6 +2820,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1632
2820
  if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1633
2821
  candidate.columns?.push(column);
1634
2822
  }
2823
+ if (table.config.internal === true) candidate.internal = true;
2824
+ if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
2825
+ if (table.config.cellMetadata === true && table.source !== void 0) {
2826
+ throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
2827
+ }
1635
2828
  if (table.config.primaryKey !== void 0) {
1636
2829
  if (columnLevelPk.length > 0) {
1637
2830
  throw new OrmDefinitionError(
@@ -1645,6 +2838,45 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1645
2838
  } else if (columnLevelPk.length > 0) {
1646
2839
  candidate.primaryKey = columnLevelPk;
1647
2840
  }
2841
+ if (table.config.cellMetadata === true) {
2842
+ candidate.cellMetadata = true;
2843
+ const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
2844
+ const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
2845
+ for (const column of tracked) {
2846
+ for (const part of parts) {
2847
+ candidate.columns?.push({
2848
+ name: cellColumnName(column.name, part),
2849
+ columnName: cellColumnName(column.columnName, part),
2850
+ type: CELL_PART_TYPES[part],
2851
+ nullable: true,
2852
+ unique: false,
2853
+ deprecated: false,
2854
+ cell: { of: column.name, part }
2855
+ });
2856
+ }
2857
+ }
2858
+ }
2859
+ if (table.config.foreignKeys?.length) {
2860
+ if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
2861
+ candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
2862
+ const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
2863
+ throw new OrmDefinitionError(message, table.file);
2864
+ } });
2865
+ if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
2866
+ return {
2867
+ name: fk.name,
2868
+ columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
2869
+ target: target.tableName,
2870
+ references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
2871
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {},
2872
+ ...fk.onDelete ? { onDelete: fk.onDelete } : {},
2873
+ ...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
2874
+ };
2875
+ });
2876
+ }
2877
+ if (table.config.checks?.length) {
2878
+ candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
2879
+ }
1648
2880
  for (const index3 of table.config.indexes ?? []) {
1649
2881
  const entry = {
1650
2882
  columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1697,6 +2929,12 @@ function buildCanonicalView({
1697
2929
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1698
2930
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1699
2931
  }
2932
+ if (column.onUpdateFn !== void 0) {
2933
+ throw new OrmDefinitionError(
2934
+ `view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
2935
+ view.file
2936
+ );
2937
+ }
1700
2938
  if (column.unique) {
1701
2939
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1702
2940
  }
@@ -1734,7 +2972,33 @@ function buildCanonicalView({
1734
2972
  columns,
1735
2973
  ...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
1736
2974
  links,
1737
- policies: { read: toCanonicalPolicy(view.config.policies.read) }
2975
+ policies: { read: toCanonicalPolicy(view.config.policies.read) },
2976
+ ...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
2977
+ ...view.config.internal === true ? { internal: true } : {}
2978
+ };
2979
+ }
2980
+ function buildCanonicalResolved(resolved) {
2981
+ const policies = {};
2982
+ if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
2983
+ if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
2984
+ if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
2985
+ return {
2986
+ sources: resolved.sources.map((source) => ({
2987
+ name: source.name,
2988
+ kind: source.kind,
2989
+ table: source.table.tableName,
2990
+ columns: [...source.columns],
2991
+ ...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
2992
+ ...source.key !== void 0 ? { key: source.key } : {},
2993
+ ...source.fetch !== void 0 ? { fetch: source.fetch } : {},
2994
+ ...source.function !== void 0 ? { function: { ...source.function } } : {},
2995
+ ...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
2996
+ ...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
2997
+ })),
2998
+ resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
2999
+ deletedBy: [...resolved.deletedBy],
3000
+ ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
3001
+ policies
1738
3002
  };
1739
3003
  }
1740
3004
  function buildCanonicalLink({
@@ -1751,6 +3015,9 @@ function buildCanonicalLink({
1751
3015
  if (link.enforce && link.linkKind !== "toOne") {
1752
3016
  return fail("sets enforce: true, which is only supported on toOne links");
1753
3017
  }
3018
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
3019
+ return fail("sets a referential action without enforce: true");
3020
+ }
1754
3021
  if (link.linkKind === "toPlatformUser") {
1755
3022
  if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
1756
3023
  return {
@@ -1779,6 +3046,8 @@ function buildCanonicalLink({
1779
3046
  on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1780
3047
  ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
1781
3048
  enforce: link.enforce,
3049
+ ...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
3050
+ ...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
1782
3051
  ...link.description !== void 0 ? { description: link.description } : {}
1783
3052
  };
1784
3053
  }
@@ -1957,6 +3226,10 @@ async function loadNamespaceFromDir(dir, options = {}) {
1957
3226
  fsCache: false,
1958
3227
  ...options.ormModulePath !== void 0 ? {
1959
3228
  alias: {
3229
+ // The public /drizzle barrel re-exports this pinned dependency.
3230
+ // Explicitly map it before the root alias: otherwise jiti appends
3231
+ // "/drizzle" to the overridden module's filename.
3232
+ "@sequenceholdings/orm/drizzle": createRequire(import.meta.url).resolve("drizzle-orm"),
1960
3233
  "@sequenceholdings/orm": options.ormModulePath,
1961
3234
  "@sequenceholdings/orm/authoring": options.ormModulePath
1962
3235
  }
@@ -2228,7 +3501,7 @@ var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
2228
3501
  var IDENTIFIER_START = /[a-z_]/;
2229
3502
  var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
2230
3503
  var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
2231
- function tokenize(source) {
3504
+ function tokenize2(source) {
2232
3505
  const tokens = [];
2233
3506
  let position = 0;
2234
3507
  while (position < source.length) {
@@ -2302,7 +3575,7 @@ function readString(source, start) {
2302
3575
  }
2303
3576
  throw new PredicateParseError("unterminated string literal", start);
2304
3577
  }
2305
- var Parser = class {
3578
+ var Parser2 = class {
2306
3579
  constructor(tokens) {
2307
3580
  this.tokens = tokens;
2308
3581
  }
@@ -2434,7 +3707,7 @@ var Parser = class {
2434
3707
  function parsePredicate(source) {
2435
3708
  const trimmed = source.trim();
2436
3709
  if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
2437
- return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
3710
+ return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
2438
3711
  }
2439
3712
 
2440
3713
  // src/schema/predicates.ts
@@ -2519,8 +3792,8 @@ var actionSchema = z.strictObject({
2519
3792
  }
2520
3793
  }
2521
3794
  });
2522
- function extractNamedParams(sql4) {
2523
- const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
3795
+ function extractNamedParams(sql7) {
3796
+ const withoutStrings = sql7.replace(QUOTED_STRING_PATTERN, "''");
2524
3797
  const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
2525
3798
  return [...new Set(names)];
2526
3799
  }
@@ -2587,17 +3860,6 @@ var SQL_TYPE_NAMES = {
2587
3860
  };
2588
3861
  var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
2589
3862
 
2590
- // src/compiler/sql.ts
2591
- var CLAIMS_SETTING = "request.jwt.claims";
2592
- var CAPABILITIES_CLAIM_KEY = "capabilities";
2593
- var ROLES_CLAIM_KEY = "roles";
2594
- var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
2595
- var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
2596
- var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
2597
- var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
2598
- var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
2599
- var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
2600
-
2601
3863
  // src/compiler/predicates.ts
2602
3864
  var SQL_COMPARE_OPS = {
2603
3865
  "=": "=",
@@ -2677,8 +3939,8 @@ var actionParamArg = (name) => `p_${name}`;
2677
3939
  function compileActionStatements(action, renderParam = actionParamArg) {
2678
3940
  return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
2679
3941
  }
2680
- function compileStatement(sql4, renderParam) {
2681
- const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
3942
+ function compileStatement(sql7, renderParam) {
3943
+ const compiled = sql7.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
2682
3944
  if (claimKey !== void 0) {
2683
3945
  if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
2684
3946
  return claimExpression(claimKey);
@@ -2743,10 +4005,10 @@ function physicalColumn(logical, options) {
2743
4005
  return options.columnNames?.[logical] ?? logical;
2744
4006
  }
2745
4007
  function compileCmp(op, column, value, options) {
2746
- const physical = physicalColumn(column, options);
2747
- const declared = options.columnTypes?.[physical] ?? options.columnTypes?.[column];
4008
+ const physical2 = physicalColumn(column, options);
4009
+ const declared = options.columnTypes?.[physical2] ?? options.columnTypes?.[column];
2748
4010
  const isEnum = typeof declared === "object" && declared !== null;
2749
- const col = isEnum ? `${quoteIdentifier(physical)}::text` : quoteIdentifier(physical);
4011
+ const col = isEnum ? `${quoteIdentifier(physical2)}::text` : quoteIdentifier(physical2);
2750
4012
  const columnType = isEnum ? "text" : declared;
2751
4013
  switch (op) {
2752
4014
  case "eq":
@@ -2790,11 +4052,11 @@ function compileEquality(col, value, columnType, negated, options) {
2790
4052
  function compileValue(value, columnType, _options) {
2791
4053
  switch (value.kind) {
2792
4054
  case "literal": {
2793
- const literal = value.value;
2794
- if (literal === null) return "NULL";
2795
- if (typeof literal === "string") return quoteLiteral(literal);
2796
- if (typeof literal === "boolean") return literal ? "TRUE" : "FALSE";
2797
- return String(literal);
4055
+ const literal2 = value.value;
4056
+ if (literal2 === null) return "NULL";
4057
+ if (typeof literal2 === "string") return quoteLiteral(literal2);
4058
+ if (typeof literal2 === "boolean") return literal2 ? "TRUE" : "FALSE";
4059
+ return String(literal2);
2798
4060
  }
2799
4061
  case "claim": {
2800
4062
  const expression = claimExpression(value.key);
@@ -2843,8 +4105,8 @@ function actionPrepareChecks(definition) {
2843
4105
  const order = actionParamOrder(action);
2844
4106
  const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
2845
4107
  const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
2846
- compiled.forEach((sql4, index3) => {
2847
- const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
4108
+ compiled.forEach((sql7, index3) => {
4109
+ const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql7) : sql7;
2848
4110
  statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
2849
4111
  });
2850
4112
  }
@@ -3226,14 +4488,14 @@ function comparisonTypeIssue(left, right, columns) {
3226
4488
  return null;
3227
4489
  }
3228
4490
  const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
3229
- const literal = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
3230
- if (!column || !literal || literal.value === null) return null;
4491
+ const literal2 = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
4492
+ if (!column || !literal2 || literal2.value === null) return null;
3231
4493
  const columnGroup = groupOf(column);
3232
4494
  if (!columnGroup) return null;
3233
- if (typeof literal.value === "string") return null;
3234
- const literalGroup = typeof literal.value === "number" ? "numeric" : "boolean";
4495
+ if (typeof literal2.value === "string") return null;
4496
+ const literalGroup = typeof literal2.value === "number" ? "numeric" : "boolean";
3235
4497
  if (literalGroup !== columnGroup) {
3236
- return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal.value} literal \u2014 incomparable types`;
4498
+ return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal2.value} literal \u2014 incomparable types`;
3237
4499
  }
3238
4500
  return null;
3239
4501
  }
@@ -3298,7 +4560,8 @@ function syncedViewStatements({
3298
4560
  readPolicyIrByTable,
3299
4561
  columnNamesByTable,
3300
4562
  columnTypesByTable,
3301
- executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
4563
+ executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE,
4564
+ backingPrivileges = "grant"
3302
4565
  }) {
3303
4566
  const schema = quoteIdentifier(ormSchemaName(definition.namespace));
3304
4567
  const role = quoteIdentifier(consumerRoleName(definition.namespace));
@@ -3318,8 +4581,8 @@ function syncedViewStatements({
3318
4581
  const backing = backingMap.get(table.table);
3319
4582
  if (!backing) continue;
3320
4583
  const viewName = `${schema}.${quoteIdentifier(table.table)}`;
3321
- const physical = splitPhysicalRelation(backing.physicalRelation);
3322
- const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
4584
+ const physical2 = splitPhysicalRelation(backing.physicalRelation);
4585
+ const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
3323
4586
  const columnTypes = mergeSyncedViewColumnTypes({
3324
4587
  table,
3325
4588
  canonicalColumnTypes: columnTypesByTable?.get(table.table),
@@ -3332,8 +4595,10 @@ function syncedViewStatements({
3332
4595
  columnTypes,
3333
4596
  columnNames: columnNamesByTable?.get(table.table)
3334
4597
  })}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
3335
- statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical.schema)} TO ${owner}`);
3336
- statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
4598
+ if (backingPrivileges === "grant") {
4599
+ statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical2.schema)} TO ${owner}`);
4600
+ statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
4601
+ }
3337
4602
  statements.push(
3338
4603
  `CREATE OR REPLACE VIEW ${viewName} AS SELECT * FROM ${physicalTarget}${whereClause}`
3339
4604
  );
@@ -3352,10 +4617,10 @@ function mergeSyncedViewColumnTypes({
3352
4617
  if (def?.type) fromTable[name] = def.type;
3353
4618
  }
3354
4619
  if (columnNames) {
3355
- for (const [logical, physical] of Object.entries(columnNames)) {
3356
- const declared = fromTable[physical] ?? fromTable[logical];
4620
+ for (const [logical, physical2] of Object.entries(columnNames)) {
4621
+ const declared = fromTable[physical2] ?? fromTable[logical];
3357
4622
  if (!declared) continue;
3358
- fromTable[physical] = declared;
4623
+ fromTable[physical2] = declared;
3359
4624
  fromTable[logical] = declared;
3360
4625
  }
3361
4626
  }
@@ -3394,8 +4659,8 @@ function syncedBackingRevokeStatements({
3394
4659
  const seen = /* @__PURE__ */ new Set();
3395
4660
  const statements = [];
3396
4661
  for (const backing of backings) {
3397
- const physical = splitPhysicalRelation(backing.physicalRelation);
3398
- const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
4662
+ const physical2 = splitPhysicalRelation(backing.physicalRelation);
4663
+ const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
3399
4664
  if (seen.has(physicalTarget)) continue;
3400
4665
  seen.add(physicalTarget);
3401
4666
  statements.push(`REVOKE SELECT ON ${physicalTarget} FROM ${owner}`);
@@ -3605,6 +4870,90 @@ function snapshotTableNames(snapshot, schema) {
3605
4870
  const tables = snapshot.tables ?? {};
3606
4871
  return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
3607
4872
  }
4873
+
4874
+ // src/engine/materialized-refresh.ts
4875
+ function materializedRefreshFunctions(canonical) {
4876
+ const schema = ormSchemaName(canonical.namespace);
4877
+ const functions = /* @__PURE__ */ new Map();
4878
+ for (const view of canonical.views ?? []) {
4879
+ const materialized = view.resolved?.materialized;
4880
+ if (!materialized) continue;
4881
+ const table = canonical.tables.find((candidate) => candidate.table === materialized.table);
4882
+ const pk = view.primaryKey ?? [];
4883
+ if (!table || pk.length === 0) {
4884
+ throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
4885
+ }
4886
+ functions.set(view.view, refreshFunctionSql({
4887
+ schema,
4888
+ name: view.view,
4889
+ table: materialized.table,
4890
+ compute: materialized.compute,
4891
+ columns: table.columns.map((column) => column.columnName),
4892
+ pk
4893
+ }));
4894
+ }
4895
+ return functions;
4896
+ }
4897
+ function refreshFunctionSql({ schema, name, table, compute, columns, pk }) {
4898
+ const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name))}`;
4899
+ const target = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
4900
+ const source = `${quoteIdentifier(schema)}.${quoteIdentifier(compute)}`;
4901
+ const list = columns.map(quoteIdentifier).join(", ");
4902
+ const others = columns.filter((column) => !pk.includes(column));
4903
+ const keys = pk.map(quoteIdentifier).join(", ");
4904
+ const matches = pk.map((column) => `c.${quoteIdentifier(column)} = m.${quoteIdentifier(column)}`).join(" AND ");
4905
+ const onConflict = others.length === 0 ? "DO NOTHING" : `DO UPDATE SET ${others.map((column) => `${quoteIdentifier(column)} = EXCLUDED.${quoteIdentifier(column)}`).join(", ")}
4906
+ WHERE (${others.map((column) => `m.${quoteIdentifier(column)}`).join(", ")}) IS DISTINCT FROM (${others.map((column) => `EXCLUDED.${quoteIdentifier(column)}`).join(", ")})`;
4907
+ const lockKey = quoteLiteral(materializeLockKey(schema, name));
4908
+ return `CREATE OR REPLACE FUNCTION ${fn}(wait boolean DEFAULT false)
4909
+ RETURNS TABLE(upserted bigint, deleted bigint, locked boolean)
4910
+ LANGUAGE plpgsql
4911
+ SECURITY INVOKER
4912
+ SET search_path = pg_catalog, pg_temp
4913
+ AS $orm_refresh$
4914
+ #variable_conflict use_column
4915
+ DECLARE
4916
+ caller_claims text := current_setting('request.jwt.claims', true);
4917
+ BEGIN
4918
+ IF wait THEN
4919
+ PERFORM pg_advisory_xact_lock(hashtext(${lockKey}));
4920
+ ELSIF NOT pg_try_advisory_xact_lock(hashtext(${lockKey})) THEN
4921
+ RETURN QUERY SELECT 0::bigint, 0::bigint, false;
4922
+ RETURN;
4923
+ END IF;
4924
+ PERFORM set_config('request.jwt.claims', ${quoteLiteral(MATERIALIZE_CLAIMS_JSON)}, true);
4925
+ RETURN QUERY
4926
+ WITH up AS (
4927
+ INSERT INTO ${target} AS m (${list})
4928
+ SELECT ${list} FROM ${source}
4929
+ ON CONFLICT (${keys}) ${onConflict}
4930
+ RETURNING 1
4931
+ ), gone AS (
4932
+ DELETE FROM ${target} AS m
4933
+ WHERE NOT EXISTS (SELECT 1 FROM ${source} AS c WHERE ${matches})
4934
+ RETURNING 1
4935
+ )
4936
+ SELECT (SELECT count(*) FROM up), (SELECT count(*) FROM gone), true;
4937
+ -- NULL resets to the session value when the caller had none.
4938
+ PERFORM set_config('request.jwt.claims', caller_claims, true);
4939
+ END
4940
+ $orm_refresh$`;
4941
+ }
4942
+ function refreshFunctionStatements({ namespace, previous, current }) {
4943
+ const schema = quoteIdentifier(ormSchemaName(namespace));
4944
+ const statements = [];
4945
+ for (const name of Object.keys(previous).sort()) {
4946
+ if (!current.has(name)) statements.push(`DROP FUNCTION IF EXISTS ${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`);
4947
+ }
4948
+ for (const [name, create] of [...current].sort(([a], [b]) => a.localeCompare(b))) {
4949
+ if (previous[name] === create) continue;
4950
+ const fn = `${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`;
4951
+ statements.push(create, `REVOKE ALL ON FUNCTION ${fn} FROM PUBLIC`);
4952
+ }
4953
+ return statements;
4954
+ }
4955
+
4956
+ // src/engine/canonical2.ts
3608
4957
  var FACTORIES2 = {
3609
4958
  uuid: (name) => uuid(name),
3610
4959
  text: (name) => text(name),
@@ -3622,7 +4971,7 @@ var FACTORIES2 = {
3622
4971
  var SCHEMA_IMPORT_KEY2 = "$schema";
3623
4972
  var enumImportKey = (name) => `$enum_${name}`;
3624
4973
  var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
3625
- function buildDrizzleEntitiesV2(definition) {
4974
+ function buildDrizzleEntitiesV2(definition, options = {}) {
3626
4975
  const schemaName = ormSchemaName(definition.namespace);
3627
4976
  const schema = pgSchema(schemaName);
3628
4977
  const imports = { [SCHEMA_IMPORT_KEY2]: schema };
@@ -3637,11 +4986,19 @@ function buildDrizzleEntitiesV2(definition) {
3637
4986
  }
3638
4987
  const byName = new Map(definition.tables.map((table) => [table.table, table]));
3639
4988
  for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
3640
- const drizzleTable = buildTable2(schema, table, built, enums, byName);
4989
+ const drizzleTable = buildTable2(schema, table, built, enums, byName, options.introspection ?? false);
3641
4990
  built.set(table.table, drizzleTable);
3642
4991
  imports[table.table] = drizzleTable;
3643
4992
  tables[table.table] = drizzleTable;
3644
4993
  }
4994
+ if (options.introspection) {
4995
+ for (const table of definition.tables.filter((table2) => table2.source)) {
4996
+ const columns = Object.fromEntries(
4997
+ table.columns.map((column) => [column.columnName, buildViewColumnV2({ view: table.table, column, enums })])
4998
+ );
4999
+ imports[table.table] = schema.view(table.table, columns).existing();
5000
+ }
5001
+ }
3645
5002
  for (const view of definition.views ?? []) {
3646
5003
  const columns = Object.fromEntries(
3647
5004
  view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
@@ -3709,18 +5066,18 @@ function rewriteSyncedSourceRelationsInViewQuery({
3709
5066
  return rewritten;
3710
5067
  });
3711
5068
  }
3712
- function transformSqlOutsideLiterals(sql4, transform) {
5069
+ function transformSqlOutsideLiterals(sql7, transform) {
3713
5070
  const out = [];
3714
5071
  let state = { kind: "normal" };
3715
5072
  let i = 0;
3716
5073
  let codeStart = 0;
3717
5074
  const emitCode = (end) => {
3718
- if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
5075
+ if (end > codeStart) out.push(transform(sql7.slice(codeStart, end)));
3719
5076
  codeStart = end;
3720
5077
  };
3721
- while (i < sql4.length) {
3722
- const c = sql4[i];
3723
- const next = i + 1 < sql4.length ? sql4[i + 1] : "";
5078
+ while (i < sql7.length) {
5079
+ const c = sql7[i];
5080
+ const next = i + 1 < sql7.length ? sql7[i + 1] : "";
3724
5081
  switch (state.kind) {
3725
5082
  case "normal": {
3726
5083
  if (c === "-" && next === "-") {
@@ -3740,7 +5097,7 @@ function transformSqlOutsideLiterals(sql4, transform) {
3740
5097
  break;
3741
5098
  }
3742
5099
  if ((c === "E" || c === "e") && next === "'") {
3743
- const prev = i > 0 ? sql4[i - 1] : "";
5100
+ const prev = i > 0 ? sql7[i - 1] : "";
3744
5101
  if (!/[A-Za-z0-9_]/.test(prev)) {
3745
5102
  emitCode(i);
3746
5103
  state = { kind: "e-single" };
@@ -3759,9 +5116,9 @@ function transformSqlOutsideLiterals(sql4, transform) {
3759
5116
  break;
3760
5117
  }
3761
5118
  if (c === "$") {
3762
- const prev = i > 0 ? sql4[i - 1] : "";
5119
+ const prev = i > 0 ? sql7[i - 1] : "";
3763
5120
  if (!/[A-Za-z0-9_$]/.test(prev)) {
3764
- const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
5121
+ const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql7.slice(i));
3765
5122
  if (tagMatch) {
3766
5123
  emitCode(i);
3767
5124
  state = { kind: "dollar", tag: tagMatch[1] ?? "" };
@@ -3793,8 +5150,8 @@ function transformSqlOutsideLiterals(sql4, transform) {
3793
5150
  break;
3794
5151
  }
3795
5152
  case "e-single": {
3796
- if (c === "\\" && i + 1 < sql4.length) {
3797
- out.push(c, sql4[i + 1]);
5153
+ if (c === "\\" && i + 1 < sql7.length) {
5154
+ out.push(c, sql7[i + 1]);
3798
5155
  i += 2;
3799
5156
  break;
3800
5157
  }
@@ -3816,7 +5173,7 @@ function transformSqlOutsideLiterals(sql4, transform) {
3816
5173
  }
3817
5174
  case "dollar": {
3818
5175
  const closer = `$${state.tag}$`;
3819
- if (sql4.startsWith(closer, i)) {
5176
+ if (sql7.startsWith(closer, i)) {
3820
5177
  out.push(closer);
3821
5178
  state = { kind: "normal" };
3822
5179
  i += closer.length;
@@ -3860,15 +5217,15 @@ function transformSqlOutsideLiterals(sql4, transform) {
3860
5217
  }
3861
5218
  }
3862
5219
  }
3863
- emitCode(sql4.length);
5220
+ emitCode(sql7.length);
3864
5221
  return out.join("");
3865
5222
  }
3866
5223
  function escapeRegExp(value) {
3867
5224
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3868
5225
  }
3869
- function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
5226
+ function sqlReferencesQualifiedRelation(sql7, sourceSchema, sourceName) {
3870
5227
  let referenced = false;
3871
- transformSqlOutsideLiterals(sql4, (segment) => {
5228
+ transformSqlOutsideLiterals(sql7, (segment) => {
3872
5229
  if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
3873
5230
  return segment;
3874
5231
  });
@@ -3879,15 +5236,18 @@ function qualifiedRelationPattern(sourceSchema, sourceName) {
3879
5236
  const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
3880
5237
  return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
3881
5238
  }
3882
- function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
3883
- return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
5239
+ function replaceQualifiedRelation(sql7, sourceSchema, sourceName, boundSchema, boundTable) {
5240
+ return sql7.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
3884
5241
  }
3885
5242
  function generateSnapshotV2(definition, previousId3) {
3886
5243
  const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
3887
5244
  const snapshot = generateDrizzleJson(imports, previousId3, [schemaName]);
3888
- if ((definition.views?.length ?? 0) > 0) {
5245
+ if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
3889
5246
  const extended = snapshot;
5247
+ const refreshFunctions = materializedRefreshFunctions(definition);
3890
5248
  extended._sequenceOrm = {
5249
+ ...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
5250
+ ...refreshFunctions.size > 0 ? { refreshFunctions: Object.fromEntries(refreshFunctions) } : {},
3891
5251
  viewDependencies: Object.fromEntries(
3892
5252
  (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
3893
5253
  )
@@ -3905,7 +5265,7 @@ function buildViewColumnV2({
3905
5265
  if (!column.nullable) builder = builder.notNull();
3906
5266
  return builder;
3907
5267
  }
3908
- function buildTable2(schema, table, built, enums, byName) {
5268
+ function buildTable2(schema, table, built, enums, byName, introspection) {
3909
5269
  const primaryKeyColumns = table.primaryKey ?? [];
3910
5270
  if (primaryKeyColumns.length === 0) {
3911
5271
  throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
@@ -3921,13 +5281,26 @@ function buildTable2(schema, table, built, enums, byName) {
3921
5281
  const columns = Object.fromEntries(
3922
5282
  table.columns.map((column) => [
3923
5283
  column.columnName,
3924
- buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
5284
+ buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums, introspection })
3925
5285
  ])
3926
5286
  );
3927
5287
  const drizzleTable = schema.table(table.table, columns, (self) => [
3928
5288
  ...buildCompositePrimaryKey(table, primaryKeyColumns, self),
3929
5289
  ...buildIndexes2(table, self),
3930
- ...buildForeignKeys2(table, self, built, byName)
5290
+ ...(table.checks ?? []).map((check$1) => check(check$1.name, sql.raw(compileCheckExpression(check$1.expression, table.columns)))),
5291
+ ...buildForeignKeys2(table, self, built, byName),
5292
+ ...(table.foreignKeys ?? []).map((fk) => {
5293
+ const target = fk.target === table.table ? self : built.get(fk.target);
5294
+ if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
5295
+ let constraint = foreignKey({
5296
+ name: fk.name,
5297
+ columns: fk.columns.map((column) => self[column]),
5298
+ foreignColumns: fk.references.map((column) => target[column])
5299
+ });
5300
+ if (fk.onDelete !== void 0) constraint = constraint.onDelete(fk.onDelete);
5301
+ if (fk.onUpdate !== void 0) constraint = constraint.onUpdate(fk.onUpdate);
5302
+ return constraint;
5303
+ })
3931
5304
  ]);
3932
5305
  return drizzleTable.enableRLS();
3933
5306
  }
@@ -3944,7 +5317,8 @@ function buildColumnV2({
3944
5317
  column,
3945
5318
  singlePrimaryKey,
3946
5319
  compositeMembers,
3947
- enums
5320
+ enums,
5321
+ introspection
3948
5322
  }) {
3949
5323
  const factory = resolveColumnFactory(table, column, enums);
3950
5324
  let builder = factory(column.columnName);
@@ -3965,7 +5339,7 @@ function buildColumnV2({
3965
5339
  }
3966
5340
  builder = builder.default(sql.raw(column.defaultFn));
3967
5341
  } else if (column.default !== void 0 && column.default !== null) {
3968
- builder = builder.default(column.default);
5342
+ builder = introspection && column.type === "jsonb" && typeof column.default === "string" ? builder.default(sql.raw(`${quoteLiteral(column.default)}::jsonb`)) : builder.default(column.default);
3969
5343
  }
3970
5344
  return builder;
3971
5345
  }
@@ -4016,11 +5390,14 @@ function buildForeignKeys2(table, self, built, byName) {
4016
5390
  `enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
4017
5391
  );
4018
5392
  }
4019
- return foreignKey({
5393
+ let constraint = foreignKey({
4020
5394
  name: foreignKeyName(table.table, link.name),
4021
5395
  columns: [sourceColumn],
4022
5396
  foreignColumns: [targetColumn]
4023
5397
  });
5398
+ if (link.onDelete !== void 0) constraint = constraint.onDelete(link.onDelete);
5399
+ if (link.onUpdate !== void 0) constraint = constraint.onUpdate(link.onUpdate);
5400
+ return constraint;
4024
5401
  });
4025
5402
  }
4026
5403
  var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
@@ -4141,16 +5518,31 @@ function findTable2(snapshot, schema, table) {
4141
5518
  const tables = snapshot.tables ?? {};
4142
5519
  return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
4143
5520
  }
4144
- async function planMigrationV2({
5521
+ function planMigrationV2(input) {
5522
+ return planWithViewRecreation({ ...input, viewRecreation: "affected" });
5523
+ }
5524
+ async function planCommittedMigrationV2({
4145
5525
  canonical,
4146
- previousSnapshot
5526
+ previousSnapshot,
5527
+ reproduces
5528
+ }) {
5529
+ const current = await planMigrationV2({ canonical, previousSnapshot });
5530
+ if (reproduces(current)) return { plan: current, reproduced: true };
5531
+ const legacy = await planWithViewRecreation({ canonical, previousSnapshot, viewRecreation: "all-on-base-change" });
5532
+ return reproduces(legacy) ? { plan: legacy, reproduced: true } : { plan: current, reproduced: false };
5533
+ }
5534
+ async function planWithViewRecreation({
5535
+ canonical,
5536
+ previousSnapshot,
5537
+ viewRecreation
4147
5538
  }) {
4148
5539
  const schema = ormSchemaName(canonical.namespace);
4149
5540
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
4150
5541
  assertValidViewDependencies(canonical);
4151
5542
  const hints = applyMigrationHintsV2(previous, canonical);
4152
5543
  const snapshot = generateSnapshotV2(canonical, previousId(previous));
4153
- const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
5544
+ const generatedBaseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
5545
+ const baseDdl = orderForeignKeyAdditions(canonical, generatedBaseDdl);
4154
5546
  const previousViews = snapshotViews(hints.adjusted);
4155
5547
  const currentViews = snapshotViews(snapshot);
4156
5548
  const previousDependencies = snapshotViewDependencies(hints.adjusted);
@@ -4160,7 +5552,14 @@ async function planMigrationV2({
4160
5552
  currentViews,
4161
5553
  previousDependencies,
4162
5554
  currentDependencies,
4163
- baseChanged: hints.statements.length > 0 || baseDdl.length > 0
5555
+ reach: viewReach({
5556
+ viewRecreation,
5557
+ hintStatements: hints.statements,
5558
+ baseDdl,
5559
+ previous,
5560
+ adjusted: hints.adjusted,
5561
+ current: snapshot
5562
+ })
4164
5563
  });
4165
5564
  const dropOrder = topologicalViewNames(
4166
5565
  new Set([...affected].filter((name) => previousViews.has(name))),
@@ -4187,15 +5586,44 @@ async function planMigrationV2({
4187
5586
  ...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
4188
5587
  ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
4189
5588
  ...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
5589
+ ...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql7) => ({ kind: "ddl", sql: sql7 })),
4190
5590
  ...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
4191
5591
  ...createOrder.map((name) => {
4192
5592
  const view = currentViewDefinitions.get(name);
4193
5593
  if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
4194
5594
  return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
4195
- })
5595
+ }),
5596
+ ...refreshFunctionStatements({
5597
+ namespace: canonical.namespace,
5598
+ previous: previous._sequenceOrm?.refreshFunctions ?? {},
5599
+ current: materializedRefreshFunctions(canonical)
5600
+ }).map((sql7) => ({ kind: "ddl", sql: sql7 }))
4196
5601
  ];
4197
5602
  return { namespace: canonical.namespace, schema, statements, snapshot };
4198
5603
  }
5604
+ function orderForeignKeyAdditions(canonical, statements) {
5605
+ if (!canonical.tables.some((table) => table.foreignKeys?.length)) return [...statements];
5606
+ const addsForeignKey = (statement) => /^ALTER TABLE[\s\S]* ADD CONSTRAINT[\s\S]* FOREIGN KEY\b/.test(statement);
5607
+ return [...statements.filter((statement) => !addsForeignKey(statement)), ...statements.filter(addsForeignKey)];
5608
+ }
5609
+ function foreignKeyTimingStatements(canonical, previous, baseDdl) {
5610
+ const prior = previous._sequenceOrm?.foreignKeys ?? {};
5611
+ const result = [];
5612
+ for (const table of canonical.tables) {
5613
+ for (const fk of table.foreignKeys ?? []) {
5614
+ const before = prior[`${table.table}.${fk.name}`];
5615
+ const recreated = baseDdl.some((statement) => statement.includes(`ALTER TABLE ${quoteIdentifier(ormSchemaName(canonical.namespace))}.${quoteIdentifier(table.table)} ADD CONSTRAINT ${quoteIdentifier(fk.name)}`));
5616
+ if (fk.deferrable ? !before || before.deferrable !== fk.deferrable || recreated : before?.deferrable && !recreated) {
5617
+ result.push(foreignKeyTimingSql(ormSchemaName(canonical.namespace), table.table, fk.name, fk.deferrable));
5618
+ }
5619
+ }
5620
+ }
5621
+ return result;
5622
+ }
5623
+ function foreignKeyTimingSql(schema, table, name, timing) {
5624
+ const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
5625
+ return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name)} ${clause}`;
5626
+ }
4199
5627
  function validateViewCatalogStatement({
4200
5628
  canonical,
4201
5629
  view,
@@ -4213,9 +5641,9 @@ function validateViewCatalogStatement({
4213
5641
  }
4214
5642
  const expectedDependencies = sqlTextArray(
4215
5643
  view.dependencies.map((dependency) => {
4216
- const physical = relationIdentity.get(dependency);
4217
- if (!physical) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
4218
- return physical;
5644
+ const physical2 = relationIdentity.get(dependency);
5645
+ if (!physical2) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
5646
+ return physical2;
4219
5647
  }).sort()
4220
5648
  );
4221
5649
  const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
@@ -4323,20 +5751,102 @@ function withoutViews(snapshot) {
4323
5751
  delete clone._sequenceOrm;
4324
5752
  return clone;
4325
5753
  }
5754
+ var DROPS_TYPE = /^\s*DROP\s+TYPE\b/i;
5755
+ function viewReach({
5756
+ viewRecreation,
5757
+ hintStatements,
5758
+ baseDdl,
5759
+ previous,
5760
+ adjusted,
5761
+ current
5762
+ }) {
5763
+ if (hintStatements.length === 0 && baseDdl.length === 0) return /* @__PURE__ */ new Set();
5764
+ if (viewRecreation === "all-on-base-change") return "all";
5765
+ if (baseDdl.some((statement) => DROPS_TYPE.test(statement))) return "all";
5766
+ const dependencies = snapshotViewDependencies(adjusted);
5767
+ if ([...snapshotViews(adjusted).keys()].some((name) => dependencies[name] === void 0)) return "all";
5768
+ return baseChangesReachingViews({ previous, adjusted, current });
5769
+ }
5770
+ var SNAPSHOT_BOOKKEEPING = /* @__PURE__ */ new Set(["id", "prevId", "version", "dialect", "_meta", "views", "_sequenceOrm", "enums"]);
5771
+ var VIEW_SAFE_TABLE_FIELDS = /* @__PURE__ */ new Set([
5772
+ "indexes",
5773
+ "foreignKeys",
5774
+ "uniqueConstraints",
5775
+ "checkConstraints",
5776
+ "policies",
5777
+ "isRLSEnabled"
5778
+ ]);
5779
+ var VIEW_SAFE_COLUMN_FIELDS = /* @__PURE__ */ new Set(["notNull", "default", "isUnique", "uniqueName", "nullsNotDistinct"]);
5780
+ function baseChangesReachingViews({
5781
+ previous,
5782
+ adjusted,
5783
+ current
5784
+ }) {
5785
+ const before = adjusted;
5786
+ const after = current;
5787
+ for (const section of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
5788
+ if (SNAPSHOT_BOOKKEEPING.has(section) || section === "tables") continue;
5789
+ if (stableJson(before[section]) !== stableJson(after[section])) return "all";
5790
+ }
5791
+ const relations = /* @__PURE__ */ new Set();
5792
+ const previousTables = snapshotTables(previous);
5793
+ const adjustedTables = snapshotTables(adjusted);
5794
+ for (const key of changedEntryKeys(previousTables, adjustedTables)) {
5795
+ const table = previousTables[key] ?? adjustedTables[key];
5796
+ if (table) relations.add(table.name);
5797
+ }
5798
+ const currentTables = snapshotTables(current);
5799
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(adjustedTables), ...Object.keys(currentTables)])) {
5800
+ const was = adjustedTables[key];
5801
+ const is2 = currentTables[key];
5802
+ if (!was || !is2) {
5803
+ const table = was ?? is2;
5804
+ if (table) relations.add(table.name);
5805
+ } else if (tableChangeReachesViews({ was, is: is2 })) {
5806
+ relations.add(is2.name);
5807
+ }
5808
+ }
5809
+ return relations;
5810
+ }
5811
+ function tableChangeReachesViews({ was, is: is2 }) {
5812
+ for (const field of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(is2)])) {
5813
+ if (field === "columns" || VIEW_SAFE_TABLE_FIELDS.has(field)) continue;
5814
+ if (stableJson(was[field]) !== stableJson(is2[field])) return true;
5815
+ }
5816
+ return Object.entries(was.columns).some(([name, column]) => {
5817
+ const next = is2.columns[name];
5818
+ if (!next) return true;
5819
+ return [.../* @__PURE__ */ new Set([...Object.keys(column), ...Object.keys(next)])].some(
5820
+ (field) => !VIEW_SAFE_COLUMN_FIELDS.has(field) && stableJson(column[field]) !== stableJson(next[field])
5821
+ );
5822
+ });
5823
+ }
5824
+ function changedEntryKeys(before, after) {
5825
+ return new Set(
5826
+ [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter(
5827
+ (key) => stableJson(before[key]) !== stableJson(after[key])
5828
+ )
5829
+ );
5830
+ }
5831
+ function snapshotTables(snapshot) {
5832
+ return snapshot.tables ?? {};
5833
+ }
4326
5834
  function affectedViewNames({
4327
5835
  previousViews,
4328
5836
  currentViews,
4329
5837
  previousDependencies,
4330
5838
  currentDependencies,
4331
- baseChanged
5839
+ reach
4332
5840
  }) {
4333
5841
  const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
4334
- if (baseChanged) return allNames;
5842
+ if (reach === "all") return allNames;
5843
+ const dependenciesOf = (name) => /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
4335
5844
  const affected = new Set(
4336
5845
  [...allNames].filter((name) => {
4337
5846
  const before = previousViews.get(name);
4338
5847
  const after = currentViews.get(name);
4339
- return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
5848
+ if (before === void 0 || after === void 0 || stableJson(before) !== stableJson(after)) return true;
5849
+ return [...dependenciesOf(name)].some((dependency) => reach.has(dependency));
4340
5850
  })
4341
5851
  );
4342
5852
  let grew = true;
@@ -4344,8 +5854,7 @@ function affectedViewNames({
4344
5854
  grew = false;
4345
5855
  for (const name of allNames) {
4346
5856
  if (affected.has(name)) continue;
4347
- const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
4348
- if ([...dependencies].some((dependency) => affected.has(dependency))) {
5857
+ if ([...dependenciesOf(name)].some((dependency) => affected.has(dependency))) {
4349
5858
  affected.add(name);
4350
5859
  grew = true;
4351
5860
  }
@@ -4450,7 +5959,63 @@ async function pendingKitStatementsV2({
4450
5959
  canonical,
4451
5960
  db
4452
5961
  }) {
4453
- return pendingStatements({ entities: buildDrizzleEntitiesV2(canonical), db, owned: V2_OWNED_STATEMENT });
5962
+ const { desired, defaultDrift } = await normalizeJsonDefaultsForIntrospection(canonical, db);
5963
+ const entities = buildDrizzleEntitiesV2(desired, { introspection: true });
5964
+ const pending = await pendingStatements({ entities, db, owned: V2_OWNED_STATEMENT });
5965
+ return [...orderForeignKeyAdditions(canonical, pending), ...defaultDrift, ...await pendingForeignKeyTimingStatements(canonical, db)];
5966
+ }
5967
+ async function normalizeJsonDefaultsForIntrospection(canonical, db) {
5968
+ const defaults = canonical.tables.flatMap((table, tableIndex) => table.source ? [] : table.columns.flatMap((column, columnIndex) => column.type === "jsonb" && typeof column.default === "string" ? [{ tableIndex, columnIndex, table: table.table, column: column.columnName, value: column.default }] : []));
5969
+ if (defaults.length === 0) return { desired: canonical, defaultDrift: [] };
5970
+ const array = (values) => sql`ARRAY[${sql.join(values.map((value) => sql`${value}`), sql`, `)}]::text[]`;
5971
+ const schema = ormSchemaName(canonical.namespace);
5972
+ const result = await shimExecute(db).execute(sql`
5973
+ SELECT desired.ordinal, desired.value::jsonb::text AS normalized,
5974
+ attribute.attnum IS NOT NULL AS column_exists,
5975
+ coalesce(pg_get_expr(ad.adbin, ad.adrelid) = quote_literal(desired.value::jsonb::text) || '::jsonb', false) AS matches
5976
+ FROM unnest(${array(defaults.map((item) => item.table))}, ${array(defaults.map((item) => item.column))}, ${array(defaults.map((item) => item.value))})
5977
+ WITH ORDINALITY AS desired(table_name, column_name, value, ordinal)
5978
+ LEFT JOIN pg_catalog.pg_namespace namespace ON namespace.nspname = ${schema}
5979
+ LEFT JOIN pg_catalog.pg_class relation ON relation.relnamespace = namespace.oid AND relation.relname = desired.table_name
5980
+ LEFT JOIN pg_catalog.pg_attribute attribute ON attribute.attrelid = relation.oid AND attribute.attname = desired.column_name AND NOT attribute.attisdropped
5981
+ LEFT JOIN pg_catalog.pg_attrdef ad ON ad.adrelid = relation.oid AND ad.adnum = attribute.attnum
5982
+ ORDER BY desired.ordinal
5983
+ `);
5984
+ const rows = result.rows;
5985
+ if (!Array.isArray(rows) || rows.length !== defaults.length || rows.some((row, index3) => Number(row.ordinal) !== index3 + 1 || typeof row.normalized !== "string" || typeof row.column_exists !== "boolean" || typeof row.matches !== "boolean")) {
5986
+ throw new Error("invalid JSON default introspection result");
5987
+ }
5988
+ const desired = structuredClone(canonical);
5989
+ const defaultDrift = [];
5990
+ defaults.forEach((item, index3) => {
5991
+ const row = rows[index3];
5992
+ desired.tables[item.tableIndex].columns[item.columnIndex].default = JSON.stringify(JSON.parse(row.normalized));
5993
+ if (row.column_exists && !row.matches) defaultDrift.push(
5994
+ `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(item.table)} ALTER COLUMN ${quoteIdentifier(item.column)} SET DEFAULT ${quoteLiteral(item.value)}::jsonb;`
5995
+ );
5996
+ });
5997
+ return { desired, defaultDrift };
5998
+ }
5999
+ async function pendingForeignKeyTimingStatements(canonical, db) {
6000
+ const keys = canonical.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => ({ table: table.table, fk })));
6001
+ if (keys.length === 0) return [];
6002
+ const schema = ormSchemaName(canonical.namespace);
6003
+ const result = await shimExecute(db).execute(sql`
6004
+ SELECT relation.relname AS table_name, constraint_row.conname AS name,
6005
+ constraint_row.condeferrable AS deferrable, constraint_row.condeferred AS deferred
6006
+ FROM pg_catalog.pg_constraint AS constraint_row
6007
+ JOIN pg_catalog.pg_class AS relation ON relation.oid = constraint_row.conrelid
6008
+ JOIN pg_catalog.pg_namespace AS namespace ON namespace.oid = relation.relnamespace
6009
+ WHERE namespace.nspname = ${schema} AND constraint_row.contype = 'f'
6010
+ `);
6011
+ const rows = result.rows;
6012
+ return keys.flatMap(({ table, fk }) => {
6013
+ const actual = rows.find((row) => row.table_name === table && row.name === fk.name);
6014
+ const deferrable = fk.deferrable !== void 0;
6015
+ const deferred = fk.deferrable === "initially-deferred";
6016
+ if (actual ? actual.deferrable === deferrable && actual.deferred === deferred : !deferrable) return [];
6017
+ return [foreignKeyTimingSql(schema, table, fk.name, fk.deferrable)];
6018
+ });
4454
6019
  }
4455
6020
  async function pendingStatements({
4456
6021
  entities,
@@ -4463,11 +6028,31 @@ async function pendingStatements({
4463
6028
  );
4464
6029
  return statementsToExecute.filter((statement) => !owned.test(statement));
4465
6030
  }
6031
+ function correctIntrospectionQuery(query) {
6032
+ if (!is(query, SQL)) return query;
6033
+ const compiled = new PgDialect().sqlToQuery(query);
6034
+ let text4 = compiled.sql;
6035
+ const indexJoin = "pg_constraint con ON con.conindid = idx.indexrelid";
6036
+ if (text4.includes("AS generated_by_constraint") && text4.includes(indexJoin)) {
6037
+ text4 = text4.replace(indexJoin, `${indexJoin} AND con.contype IN ('p', 'u', 'x')`);
6038
+ }
6039
+ const sourceJoin = "LEFT JOIN pg_catalog.pg_attribute att ON att.attnum = ANY (con.conkey)";
6040
+ const targetJoin = "LEFT JOIN pg_catalog.pg_attribute fatt ON fatt.attnum = ANY (con.confkey)";
6041
+ if (text4.includes(sourceJoin) && text4.includes(targetJoin) && text4.includes("AND con.contype IN ('f')")) {
6042
+ text4 = text4.replace(
6043
+ sourceJoin,
6044
+ "JOIN LATERAL unnest(con.conkey, con.confkey) WITH ORDINALITY AS fk_key(source_num, target_num, ordinal) ON true LEFT JOIN pg_catalog.pg_attribute att ON att.attnum = fk_key.source_num"
6045
+ ).replace(targetJoin, "LEFT JOIN pg_catalog.pg_attribute fatt ON fatt.attnum = fk_key.target_num").replace(/;\s*$/, "") + " ORDER BY con.conname, fk_key.ordinal";
6046
+ }
6047
+ if (text4 === compiled.sql) return query;
6048
+ if (compiled.params.length !== 0) throw new Error("parameterized Drizzle catalog query cannot be normalized");
6049
+ return sql.raw(text4);
6050
+ }
4466
6051
  function shimExecute(db) {
4467
6052
  const execute = db.execute.bind(db);
4468
6053
  return {
4469
6054
  execute: async (query) => {
4470
- const result = await execute(query);
6055
+ const result = await execute(correctIntrospectionQuery(query));
4471
6056
  if (result && typeof result === "object" && "rows" in result) return result;
4472
6057
  return { rows: result };
4473
6058
  }
@@ -4507,21 +6092,73 @@ async function planMigration({
4507
6092
  const ddl = await diffStatements(hints.adjusted, snapshot);
4508
6093
  const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
4509
6094
  const statements = [
4510
- ...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
4511
- ...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
4512
- ...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
4513
- ...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
6095
+ ...policyDrops.map((sql7) => ({ kind: "security", sql: sql7 })),
6096
+ ...hints.statements.map((sql7) => ({ kind: "hint", sql: sql7 })),
6097
+ ...ddl.map((sql7) => ({ kind: "ddl", sql: sql7.replace(/;\s*$/, "") })),
6098
+ ...securityStatements(definition).map((sql7) => ({ kind: "security", sql: sql7 })),
4514
6099
  // PREPARE each action's executes first (plpgsql bodies don't check columns
4515
6100
  // at CREATE), then create the functions. Both reference the tables the DDL
4516
6101
  // above creates, so they close the plan.
4517
- ...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
4518
- ...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
6102
+ ...actionPrepareChecks(definition).map((sql7) => ({ kind: "function", sql: sql7 })),
6103
+ ...actionFunctionStatements(definition).map((sql7) => ({ kind: "function", sql: sql7 }))
4519
6104
  ];
4520
6105
  return { namespace: definition.namespace, schema, statements, snapshot };
4521
6106
  }
4522
6107
  var compileNamespace = (definition) => planMigration({ definition });
4523
6108
  var previousId2 = (snapshot) => snapshot.id;
4524
- var IR_VERSION = 1;
6109
+ var ORM_MIGRATION_EXPORT_FILE = "orm.migration.json";
6110
+ var ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
6111
+ var hash = z.string().regex(/^sha256:[0-9a-f]{64}$/);
6112
+ var ormMigrationPayloadSchema = z.object({
6113
+ canonical_definition: canonicalDefinitionSchema,
6114
+ documents: z.array(z.object({ path: z.string().min(1).max(2048), contents: z.string().max(1e6) }).strict()).max(5e3),
6115
+ client_manifest: z.object({ ops_hash: hash, operations: z.record(z.string(), hash) }).strict(),
6116
+ sdl: z.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_GENERATED_SDL_BYTES, {
6117
+ message: `sdl exceeds ${MAX_GENERATED_SDL_BYTES} bytes`
6118
+ }),
6119
+ schema_snapshot: z.json(),
6120
+ // The existing ORM committed-migration validator and replay engine remain
6121
+ // authoritative. This envelope hashes the provenance, never authorizes SQL.
6122
+ migration: z.json().optional()
6123
+ }).strict();
6124
+ var ormMigrationExportSchema = z.object({
6125
+ schema_version: z.literal(1),
6126
+ schema_hash: hash,
6127
+ payload: ormMigrationPayloadSchema,
6128
+ checksum: z.string().regex(/^[0-9a-f]{64}$/)
6129
+ }).strict();
6130
+ function checksum(value) {
6131
+ return createHash("sha256").update(stableJson(value)).digest("hex");
6132
+ }
6133
+ function createOrmMigrationExport({ schemaHash, payload }) {
6134
+ const contents = { schema_version: 1, schema_hash: hash.parse(schemaHash), payload: ormMigrationPayloadSchema.parse(payload) };
6135
+ return { ...contents, checksum: checksum(contents) };
6136
+ }
6137
+ function parseOrmMigrationExport(text4) {
6138
+ if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
6139
+ const exported = ormMigrationExportSchema.parse(JSON.parse(text4));
6140
+ const { checksum: expected, ...contents } = exported;
6141
+ if (checksum(contents) !== expected) throw new Error("ORM migration export checksum mismatch; regenerate and commit it");
6142
+ return exported;
6143
+ }
6144
+ function renderOrmMigrationExport(exported) {
6145
+ const text4 = `${JSON.stringify(exported, null, 2)}
6146
+ `;
6147
+ if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
6148
+ return text4;
6149
+ }
6150
+ var IR_VERSION = 2;
6151
+ var LEGACY_IR_VERSION = 1;
6152
+ var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(IR_VERSION)]);
6153
+ var operationVersionSchema = z.object({ irVersion: irVersionSchema });
6154
+ function requiredArtifactIrVersion(operations) {
6155
+ let required = LEGACY_IR_VERSION;
6156
+ for (const operation of operations) {
6157
+ const { irVersion } = operationVersionSchema.parse(operation);
6158
+ if (irVersion > required) required = irVersion;
6159
+ }
6160
+ return required;
6161
+ }
4525
6162
  var CLAIMS_CONTRACT = {
4526
6163
  sub: "string",
4527
6164
  org: "string?",
@@ -4593,6 +6230,21 @@ var selectionIrSchema = z.lazy(
4593
6230
  () => z.object({
4594
6231
  typename: z.string(),
4595
6232
  columns: z.array(z.object({ alias: z.string(), column: z.string() })),
6233
+ cells: z.object({
6234
+ alias: z.string(),
6235
+ typename: z.string(),
6236
+ fields: z.array(z.object({
6237
+ alias: z.string(),
6238
+ column: z.string(),
6239
+ typename: z.string(),
6240
+ parts: z.array(z.object({
6241
+ alias: z.string(),
6242
+ part: z.enum(["source", "at", "by", "conflicts"]),
6243
+ fields: z.array(z.object({ alias: z.string(), field: z.enum(["source", "at", "value"]) })).optional(),
6244
+ typename: z.string().optional()
6245
+ }))
6246
+ }))
6247
+ }).optional(),
4596
6248
  relations: z.array(z.object({
4597
6249
  alias: z.string(),
4598
6250
  link: z.string(),
@@ -4614,6 +6266,11 @@ var selectionIrSchema = z.lazy(
4614
6266
  })
4615
6267
  );
4616
6268
  var includeIfSchema = z.object({ variable: z.string(), negated: z.boolean() });
6269
+ var prefetchPlanIrSchema = z.object({
6270
+ source: z.string(),
6271
+ maxStalenessMs: z.number().int().positive().optional(),
6272
+ key: z.union([z.object({ one: valueIrSchema }), z.object({ many: valueIrSchema })])
6273
+ });
4617
6274
  var selectRootSchema = z.object({
4618
6275
  kind: z.literal("select"),
4619
6276
  alias: z.string(),
@@ -4625,7 +6282,8 @@ var selectRootSchema = z.object({
4625
6282
  distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
4626
6283
  limit: limitIrSchema,
4627
6284
  offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
4628
- includeIf: includeIfSchema.optional()
6285
+ includeIf: includeIfSchema.optional(),
6286
+ prefetch: z.array(prefetchPlanIrSchema).optional()
4629
6287
  });
4630
6288
  var byPkRootSchema = z.object({
4631
6289
  kind: z.literal("byPk"),
@@ -4635,7 +6293,8 @@ var byPkRootSchema = z.object({
4635
6293
  pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
4636
6294
  /** Policy predicate — ANDed with the PK match. */
4637
6295
  where: predIrSchema,
4638
- includeIf: includeIfSchema.optional()
6296
+ includeIf: includeIfSchema.optional(),
6297
+ prefetch: z.array(prefetchPlanIrSchema).optional()
4639
6298
  });
4640
6299
  var aggregateRootSchema = z.object({
4641
6300
  kind: z.literal("aggregate"),
@@ -4648,6 +6307,33 @@ var aggregateRootSchema = z.object({
4648
6307
  nodes: selectionIrSchema.optional(),
4649
6308
  includeIf: includeIfSchema.optional()
4650
6309
  });
6310
+ var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
6311
+ var groupByKeyIrSchema = z.object({
6312
+ column: z.string(),
6313
+ bucket: z.enum(DATE_BUCKETS).optional()
6314
+ });
6315
+ var aggregateGroupsRootSchema = z.object({
6316
+ kind: z.literal("aggregateGroups"),
6317
+ alias: z.string(),
6318
+ table: z.string(),
6319
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
6320
+ where: predIrSchema,
6321
+ groupBy: z.array(groupByKeyIrSchema).min(1),
6322
+ /**
6323
+ * The `key { … }` projection: which group columns appear in each entry's key
6324
+ * object, in selection order. A subset of `groupBy` (the compiler rejects a
6325
+ * key field that is not a group key — it would be indeterminate within the
6326
+ * group). Empty when the document selects only `aggregate`.
6327
+ */
6328
+ key: z.array(z.object({ alias: z.string(), column: z.string() })).default([]),
6329
+ count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
6330
+ sum: z.array(z.string()).optional(),
6331
+ avg: z.array(z.string()).optional(),
6332
+ /** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
6333
+ limit: limitIrSchema,
6334
+ offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
6335
+ includeIf: includeIfSchema.optional()
6336
+ });
4651
6337
  var writeValuesSchema = z.union([
4652
6338
  z.object({ slot: z.string() }),
4653
6339
  z.object({ entries: z.array(z.object({ column: z.string(), value: valueIrSchema })) })
@@ -4666,7 +6352,20 @@ var mutationRootSchema = z.object({
4666
6352
  * coalesce view falls back to synced. Both are gated on a matching synced
4667
6353
  * row and carry the write policy in `where` like update/delete.
4668
6354
  */
4669
- op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
6355
+ op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
6356
+ /**
6357
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
6358
+ * executor gates on the resolved row (write policy in `where`), writes only
6359
+ * the touched cells into the manual source, then re-checks `insertCheck`
6360
+ * against the resolved row. `revert` clears `revertColumns` from the manual
6361
+ * source; `delete` tombstones the row there.
6362
+ */
6363
+ resolvedAction: z.enum(["insert", "update", "delete", "revert"]).optional(),
6364
+ revertColumns: z.array(z.string()).optional(),
6365
+ /** `@writeback(source:, mode:)` directives on this root. */
6366
+ writeback: z.array(z.object({ source: z.string(), mode: z.enum(["sync", "async"]) })).optional(),
6367
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
6368
+ localOnly: z.array(z.string()).optional(),
4670
6369
  alias: z.string(),
4671
6370
  table: z.string(),
4672
6371
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -4693,12 +6392,15 @@ var mutationRootSchema = z.object({
4693
6392
  insertCheck: predIrSchema.optional(),
4694
6393
  returning: selectionIrSchema.nullable(),
4695
6394
  affectedRows: z.boolean().default(false),
6395
+ /** v2: abort the transaction when the actual write count differs, regardless of selection. */
6396
+ assertAffectedRows: z.number().int().min(0).max(2147483647).optional(),
4696
6397
  includeIf: includeIfSchema.optional()
4697
6398
  });
4698
6399
  var rootIrSchema = z.discriminatedUnion("kind", [
4699
6400
  selectRootSchema,
4700
6401
  byPkRootSchema,
4701
6402
  aggregateRootSchema,
6403
+ aggregateGroupsRootSchema,
4702
6404
  mutationRootSchema
4703
6405
  ]);
4704
6406
  var INPUT_REF_KINDS = [
@@ -4728,7 +6430,7 @@ var variableIrSchema = z.object({
4728
6430
  defaultValue: variableDefaultIrSchema.optional()
4729
6431
  });
4730
6432
  var compiledOperationSchema = z.object({
4731
- irVersion: z.literal(IR_VERSION),
6433
+ irVersion: irVersionSchema,
4732
6434
  name: z.string(),
4733
6435
  opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
4734
6436
  kind: z.enum(["query", "mutation"]),
@@ -4740,6 +6442,50 @@ var compiledOperationSchema = z.object({
4740
6442
  /** Every table the op touches — data-token scoping + audit. */
4741
6443
  tableSet: z.array(z.string()).min(1),
4742
6444
  roots: z.array(rootIrSchema).min(1)
6445
+ }).superRefine((operation, ctx) => {
6446
+ for (const [index3, root] of operation.roots.entries()) {
6447
+ if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
6448
+ if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
6449
+ ctx.addIssue({ code: "custom", path: ["roots", index3], message: "affected-row assertions require unconditional mutation roots and IR v2" });
6450
+ }
6451
+ }
6452
+ });
6453
+ var resolvedSourceIrSchema = z.object({
6454
+ name: z.string(),
6455
+ kind: z.enum(["manual", "etl", "prefetch"]),
6456
+ /** Logical table key of the backing relation. */
6457
+ table: z.string(),
6458
+ columns: z.array(z.string()),
6459
+ key: z.string().optional(),
6460
+ fetch: z.union([
6461
+ z.object({ shape: z.literal("single") }),
6462
+ z.object({ shape: z.literal("batch"), maxKeys: z.number().int().positive() })
6463
+ ]).optional(),
6464
+ function: z.object({ kind: z.enum(["managed", "native"]), id: z.string() }).optional(),
6465
+ prefetchDefaults: z.object({
6466
+ maxStalenessMs: z.number().int().positive(),
6467
+ timeoutMs: z.number().int().positive(),
6468
+ onError: z.enum(["fallback", "fail"])
6469
+ }).optional(),
6470
+ writeback: z.object({
6471
+ columns: z.array(z.string()),
6472
+ defaultMode: z.enum(["sync", "async"]),
6473
+ timeoutMs: z.number().int().positive(),
6474
+ clearManualOnConfirm: z.boolean()
6475
+ }).optional()
6476
+ });
6477
+ var resolvedMetaIrSchema = z.object({
6478
+ /** Logical pk column of the resolved view (single-column). */
6479
+ pk: z.string(),
6480
+ /** Source name of the manual source. */
6481
+ manual: z.string(),
6482
+ sources: z.array(resolvedSourceIrSchema),
6483
+ deletedBy: z.array(z.string()),
6484
+ /**
6485
+ * Materialized: the view reads `table`, which the data plane refreshes from
6486
+ * the `compute` view (claims-gated) whenever it writes a source row.
6487
+ */
6488
+ materialized: z.object({ table: z.string(), compute: z.string() }).optional()
4743
6489
  });
4744
6490
  var tableMetaIrSchema = z.object({
4745
6491
  table: z.string(),
@@ -4776,18 +6522,45 @@ var tableMetaIrSchema = z.object({
4776
6522
  insertable: z.array(z.string()),
4777
6523
  settable: z.array(z.string()),
4778
6524
  appendable: z.array(z.string()),
6525
+ /**
6526
+ * Logical columns stamped with Postgres `now()` on UPDATE (canonical
6527
+ * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
6528
+ */
6529
+ touchOnUpdate: z.array(z.string()).optional(),
6530
+ /**
6531
+ * Per-cell metadata: logical column → the logical names of its cell-part
6532
+ * columns (which are listed in `columns`, but never filterable, orderable or
6533
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
6534
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
6535
+ * the relation tracks no cells.
6536
+ */
6537
+ cells: z.record(z.string(), z.object({
6538
+ at: z.string().optional(),
6539
+ by: z.string().optional(),
6540
+ rev: z.string().optional(),
6541
+ source: z.string().optional(),
6542
+ conflicts: z.string().optional()
6543
+ }).strict()).optional(),
6544
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
6545
+ resolved: resolvedMetaIrSchema.optional(),
4779
6546
  constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
4780
6547
  /** Depth/node caps for dynamic filter trees on this table. */
4781
6548
  dynamicFilterMaxDepth: z.number().int().positive().default(6),
4782
6549
  dynamicFilterMaxNodes: z.number().int().positive().default(64)
4783
6550
  });
4784
6551
  var compiledArtifactSchema = z.object({
4785
- irVersion: z.literal(IR_VERSION),
6552
+ irVersion: irVersionSchema,
4786
6553
  namespace: z.string(),
4787
6554
  schemaHash: z.string(),
4788
6555
  opsHash: z.string(),
4789
6556
  tables: z.record(z.string(), tableMetaIrSchema),
4790
6557
  operations: z.record(z.string(), compiledOperationSchema)
6558
+ }).superRefine((artifact, ctx) => {
6559
+ for (const [name, operation] of Object.entries(artifact.operations)) {
6560
+ if (operation.irVersion > artifact.irVersion) {
6561
+ ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
6562
+ }
6563
+ }
4791
6564
  });
4792
6565
  var IrVersionError = class extends Error {
4793
6566
  constructor(found) {
@@ -4798,7 +6571,7 @@ var IrVersionError = class extends Error {
4798
6571
  function normalizeIr(raw) {
4799
6572
  if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
4800
6573
  const version = raw.irVersion;
4801
- if (version !== IR_VERSION) throw new IrVersionError(version);
6574
+ if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
4802
6575
  return compiledArtifactSchema.parse(raw);
4803
6576
  }
4804
6577
  function indexByHash(artifact) {
@@ -5114,12 +6887,12 @@ async function writeMigration({
5114
6887
  const tip = workspace.journal.entries.at(-1);
5115
6888
  const ordinal = (tip?.ordinal ?? 0) + 1;
5116
6889
  const name = migrationName(ordinal, contentHash);
5117
- const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
6890
+ const sql7 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
5118
6891
  const metaDir = join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
5119
6892
  await mkdir(metaDir, { recursive: true });
5120
6893
  const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
5121
6894
  `;
5122
- await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
6895
+ await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql7);
5123
6896
  await writeFile(join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
5124
6897
  const journal = {
5125
6898
  ...workspace.journal,
@@ -5130,7 +6903,7 @@ async function writeMigration({
5130
6903
  name,
5131
6904
  content_hash: contentHash,
5132
6905
  previous_content_hash: tip?.content_hash ?? null,
5133
- checksum: checksumOf(sql4),
6906
+ checksum: checksumOf(sql7),
5134
6907
  snapshot_checksum: checksumOf(snapshotText),
5135
6908
  destructive
5136
6909
  }
@@ -5245,8 +7018,8 @@ async function checkWorkspace({
5245
7018
  let previousId3;
5246
7019
  let previousHash = null;
5247
7020
  for (const entry of workspace.journal.entries) {
5248
- const sql4 = await readMigrationSql(dir, entry);
5249
- if (checksumOf(sql4) !== entry.checksum) {
7021
+ const sql7 = await readMigrationSql(dir, entry);
7022
+ if (checksumOf(sql7) !== entry.checksum) {
5250
7023
  log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
5251
7024
  return 1;
5252
7025
  }
@@ -5286,7 +7059,7 @@ async function checkWorkspace({
5286
7059
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
5287
7060
  return 0;
5288
7061
  }
5289
- var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
7062
+ var firstLine = (sql7) => sql7.split("\n")[0] ?? sql7;
5290
7063
 
5291
7064
  // src/migrations/from-yaml.ts
5292
7065
  function convertV1DefinitionToCanonical(definition) {
@@ -5392,7 +7165,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
5392
7165
  throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
5393
7166
  }
5394
7167
  const name = migrationName(ordinal, newHash);
5395
- const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
7168
+ const sql7 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
5396
7169
  const snapshotText = `${JSON.stringify(snapshot, null, 2)}
5397
7170
  `;
5398
7171
  const entry = {
@@ -5400,11 +7173,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
5400
7173
  name,
5401
7174
  content_hash: newHash,
5402
7175
  previous_content_hash: previousHash,
5403
- checksum: checksumOf(sql4),
7176
+ checksum: checksumOf(sql7),
5404
7177
  snapshot_checksum: checksumOf(snapshotText),
5405
7178
  destructive: false
5406
7179
  };
5407
- return { name, entry, sql: sql4, snapshotText };
7180
+ return { name, entry, sql: sql7, snapshotText };
5408
7181
  }
5409
7182
  function renderRebaseMigrationSql({
5410
7183
  name,
@@ -5502,6 +7275,6 @@ function canonicalActions(value) {
5502
7275
  );
5503
7276
  }
5504
7277
 
5505
- export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CANONICAL_FORMAT, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CLAIMS_CONTRACT, CLAIMS_SETTING, CLAIM_KEY_PATTERN, CMP_OPS, COLUMN_BRAND, COLUMN_DEFAULT_FNS, COLUMN_TYPES2 as COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, ColumnBuilder, DEFINITION_FILE_EXTENSIONS, ENUM_BRAND, HASH_DOMAIN, IDENTIFIER_PATTERN, IDENTIFIER_RE, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LINK_BRAND, MAX_DEFINITION_FILE_BYTES, MAX_IDENTIFIER_LENGTH, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, ORM_TO_PIPELINE_TYPE, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, ROLES_CLAIM_KEY, SCHEMA_DIR, STATEMENT_KINDS, SYNCED_CONTRACT_HASH_DOMAIN, SYNCED_SOURCE_PATTERN, SYNCED_VIEW_EXECUTION_OWNER_ROLE, TABLE_BRAND, VIEW_BRAND, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, activeAuthoringContext, applyMigrationHints, applyMigrationHintsV2, beginAuthoringContext, buildActionParamsSchema, buildDrizzleEntities, buildDrizzleEntitiesV2, buildNamespaceDefinition, buildRebaseJournalEntry, canonicalActionSchema, canonicalColumnSchema, canonicalDefinitionSchema, canonicalEnumSchema, canonicalIndexSchema, canonicalLinkSchema, canonicalPoliciesSchema, canonicalRelations, canonicalTableSchema, canonicalViewSchema, canonicalizeDefinition, capabilityExpression, castParts, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema2 as columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredIrToSql, compilePredicate, compiledArtifactSchema, compiledOperationSchema, computeCanonicalHash, computeDefinitionHash, computeSyncedContractHash, consumerRoleName, convertV1DefinitionToCanonical, declaredCapabilityKeys, defineEnum, defineNamespace, defineSyncedTable, defineTable, defineView, deriveColumnName, diffCommand, diffStatements, emptyJournal, emptySnapshot, endAuthoringContext, extractNamedParams, extractOrmSyncedTableContract, generateNamespaceTypes, generateSnapshot, generateSnapshotV2, identifierSchema, indexByHash, isColumnBuilder, isEnumRef, isFunctionDefault, isOwnedStatement, isOwnedStatementV2, isQualifiedReference, isSeqLink, isSeqNamespaceConfig, isSeqTable, isSeqView, limitIrSchema, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, loadNamespaceFromDir, migrationName, namespaceConfigSchema, namespaceSchema, normalizeCanonical, normalizeIr, normalizeSnapshot, orderIrSchema, ormSchemaName, overlayWinsUnion, overrideRowMutation, parseCanonicalDefinition, parseDefinitionSources, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, pendingKitStatementsV2, planMigration, planMigrationV2, policyExpressionSchema, policyVariantSchema, predIrSchema, predicateOperands, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, rebaseViolations, referencedCapabilities, referencedColumns, renderMigrationSql, revertOverlayMutation, rewriteSyncedSourceRelationsInViewQuery, roleExpression, rootIrSchema, securityStatements, selectionIrSchema, serializeNamespace, sqlTypeName, stableJson, syncedBackingRevokeStatements, syncedContractSurface, syncedTables, syncedViewDropStatements, syncedViewStatements, t, tableFileSchema, tableMetaIrSchema, tableReferenceSchema, toMany, toOne, toPlatformUser, typesInterfaceName, validateViewCatalogStatement, valueIrSchema, variableDefaultIrSchema, variableIrSchema, verifyRebaseIsNoop, walkPredicate, writeMigration };
7278
+ export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CANONICAL_FORMAT, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CELLS_FIELD, CELL_PARTS, CELL_PART_SUFFIX, CLAIMS_CONTRACT, CLAIMS_SETTING, CLAIM_KEY_PATTERN, CMP_OPS, COLUMN_BRAND, COLUMN_DEFAULT_FNS, COLUMN_IDENTIFIER_PATTERN, COLUMN_TYPES2 as COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, ColumnBuilder, DATE_BUCKETS, DEFINITION_FILE_EXTENSIONS, ENUM_BRAND, HASH_DOMAIN, IDENTIFIER_PATTERN, IDENTIFIER_RE, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LEGACY_IR_VERSION, LINK_BRAND, MATERIALIZE_ACTOR, MATERIALIZE_CLAIMS_JSON, MAX_DEFINITION_FILE_BYTES, MAX_GENERATED_SDL_BYTES, MAX_IDENTIFIER_LENGTH, MAX_TRACKED_COLUMN_NAME, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_MIGRATION_EXPORT_FILE, ORM_MIGRATION_EXPORT_MAX_BYTES, ORM_REGISTRATION_MAX_BYTES, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, ORM_TO_PIPELINE_TYPE, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, ROLES_CLAIM_KEY, SCHEMA_DIR, STATEMENT_KINDS, SYNCED_CONTRACT_HASH_DOMAIN, SYNCED_SOURCE_PATTERN, SYNCED_VIEW_EXECUTION_OWNER_ROLE, TABLE_BRAND, VIEW_BRAND, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, activeAuthoringContext, applyMigrationHints, applyMigrationHintsV2, beginAuthoringContext, buildActionParamsSchema, buildDrizzleEntities, buildDrizzleEntitiesV2, buildNamespaceDefinition, buildRebaseJournalEntry, canonicalActionSchema, canonicalColumnSchema, canonicalDefinitionSchema, canonicalEnumSchema, canonicalIndexSchema, canonicalLinkSchema, canonicalPoliciesSchema, canonicalRelations, canonicalTableSchema, canonicalViewSchema, canonicalizeDefinition, capabilityExpression, castParts, cellColumnName, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema2 as columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredIrToSql, compilePredicate, compiledArtifactSchema, compiledOperationSchema, computeCanonicalHash, computeDefinitionHash, computeSyncedContractHash, consumerRoleName, convertV1DefinitionToCanonical, createOrmMigrationExport, custom, declaredCapabilityKeys, defineEnum, defineNamespace, defineResolvedTable, defineSyncedTable, defineTable, defineView, deriveColumnName, diffCommand, diffStatements, emptyJournal, emptySnapshot, endAuthoringContext, etlSource, extractNamedParams, extractOrmSyncedTableContract, generateNamespaceTypes, generateSnapshot, generateSnapshotV2, highest, identifierSchema, indexByHash, isColumnBuilder, isEnumRef, isFunctionDefault, isOwnedStatement, isOwnedStatementV2, isQualifiedReference, isSeqLink, isSeqNamespaceConfig, isSeqTable, isSeqView, latest, limitIrSchema, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, loadNamespaceFromDir, lowest, managedFn, manualSource, materializeLockKey, materializedRefreshFunctionName, migrationName, namespaceConfigSchema, namespaceSchema, nonNullFirst, normalizeCanonical, normalizeIr, normalizeSnapshot, orderIrSchema, ormMigrationExportSchema, ormSchemaName, overlayWinsUnion, overrideRowMutation, parseCanonicalDefinition, parseDefinitionSources, parseOrmMigrationExport, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, pendingKitStatementsV2, planCommittedMigrationV2, planMigration, planMigrationV2, policyExpressionSchema, policyVariantSchema, predIrSchema, predicateOperands, prefetchPlanIrSchema, prefetchSource, priority, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, rebaseViolations, referencedCapabilities, referencedColumns, renderMigrationSql, renderOrmMigrationExport, requiredArtifactIrVersion, resolvedMetaIrSchema, resolvedSourceIrSchema, revertOverlayMutation, rewriteSyncedSourceRelationsInViewQuery, roleExpression, rootIrSchema, securityStatements, selectionIrSchema, serializeNamespace, sqlTypeName, stableJson, syncedBackingRevokeStatements, syncedContractSurface, syncedTables, syncedViewDropStatements, syncedViewStatements, t, tableFileSchema, tableMetaIrSchema, tableReferenceSchema, toMany, toOne, toPlatformUser, typesInterfaceName, validateViewCatalogStatement, valueIrSchema, variableDefaultIrSchema, variableIrSchema, verifyRebaseIsNoop, walkPredicate, writeMigration };
5506
7279
  //# sourceMappingURL=index.js.map
5507
7280
  //# sourceMappingURL=index.js.map