@sequenceholdings/orm 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +94 -2
  2. package/dist/authoring.cjs +1474 -54
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +242 -4
  5. package/dist/authoring.d.ts +242 -4
  6. package/dist/authoring.js +1465 -56
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +740 -42
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +742 -53
  11. package/dist/canonical.d.ts +742 -53
  12. package/dist/canonical.js +724 -43
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +2478 -328
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +2480 -330
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs +90 -25
  19. package/dist/client.cjs.map +1 -1
  20. package/dist/client.d.cts +39 -22
  21. package/dist/client.d.ts +39 -22
  22. package/dist/client.js +87 -26
  23. package/dist/client.js.map +1 -1
  24. package/dist/compiler.cjs +1755 -329
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +56 -3
  27. package/dist/compiler.d.ts +56 -3
  28. package/dist/compiler.js +1756 -330
  29. package/dist/compiler.js.map +1 -1
  30. package/dist/constants-YoFGjIBM.d.cts +99 -0
  31. package/dist/constants-YoFGjIBM.d.ts +99 -0
  32. package/dist/dynamic-input.d.cts +1 -0
  33. package/dist/dynamic-input.d.ts +1 -0
  34. package/dist/index.cjs +1973 -164
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +474 -100
  37. package/dist/index.d.ts +474 -100
  38. package/dist/index.js +1940 -167
  39. package/dist/index.js.map +1 -1
  40. package/dist/ir.cjs +869 -46
  41. package/dist/ir.cjs.map +1 -1
  42. package/dist/ir.d.cts +699 -25
  43. package/dist/ir.d.ts +699 -25
  44. package/dist/ir.js +861 -47
  45. package/dist/ir.js.map +1 -1
  46. package/dist/{naming-CTvWSRTf.d.ts → naming-CPfx7EfF.d.ts} +28 -1
  47. package/dist/{naming-DRyinU4u.d.cts → naming-CU9ThP_E.d.cts} +28 -1
  48. package/dist/policy-1iZbGnZZ.d.cts +24 -0
  49. package/dist/policy-1iZbGnZZ.d.ts +24 -0
  50. package/dist/predicate-v2.cjs +852 -49
  51. package/dist/predicate-v2.cjs.map +1 -1
  52. package/dist/predicate-v2.d.cts +2 -1
  53. package/dist/predicate-v2.d.ts +2 -1
  54. package/dist/predicate-v2.js +852 -49
  55. package/dist/predicate-v2.js.map +1 -1
  56. package/dist/sdl.cjs +1309 -58
  57. package/dist/sdl.cjs.map +1 -1
  58. package/dist/sdl.d.cts +7 -3
  59. package/dist/sdl.d.ts +7 -3
  60. package/dist/sdl.js +1305 -59
  61. package/dist/sdl.js.map +1 -1
  62. package/package.json +11 -6
  63. package/templates/orm-namespace/CLAUDE.md +6 -4
  64. package/templates/orm-namespace/codegen.preset.d.cts +7 -0
  65. package/templates/orm-namespace/codegen.ts +2 -1
  66. package/templates/orm-namespace/graphql/example/queries.ts +4 -3
  67. package/templates/orm-namespace/package.json +2 -1
  68. package/templates/orm-namespace/schema/tables.ts +3 -2
  69. package/templates/orm-namespace/sequence.config.ts +7 -11
@@ -2,10 +2,489 @@
2
2
 
3
3
  var zod = require('zod');
4
4
 
5
+ // src/canonical/v2.ts
6
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
7
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
8
+
9
+ // src/checks/compile.ts
10
+ function tokenize(source) {
11
+ const tokens = [];
12
+ for (let position = 0; position < source.length; ) {
13
+ const char = source[position];
14
+ if (/\s/.test(char)) {
15
+ position++;
16
+ continue;
17
+ }
18
+ if (char === "(" || char === ")" || char === ",") {
19
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
20
+ position++;
21
+ continue;
22
+ }
23
+ if (char === "'") {
24
+ const start = position++;
25
+ let value = "";
26
+ let closed = false;
27
+ while (position < source.length) {
28
+ if (source[position] !== "'") {
29
+ value += source[position++];
30
+ continue;
31
+ }
32
+ if (source[position + 1] === "'") {
33
+ value += "'";
34
+ position += 2;
35
+ continue;
36
+ }
37
+ position++;
38
+ closed = true;
39
+ break;
40
+ }
41
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
42
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
43
+ tokens.push({ kind: "string", value, position: start });
44
+ continue;
45
+ }
46
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
47
+ if (operator) {
48
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
49
+ position += operator[0].length;
50
+ continue;
51
+ }
52
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
53
+ if (number) {
54
+ tokens.push({ kind: "number", value: number[0], position });
55
+ position += number[0].length;
56
+ continue;
57
+ }
58
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
59
+ if (identifier3) {
60
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
61
+ position += identifier3[0].length;
62
+ continue;
63
+ }
64
+ throw new Error(`unexpected character '${char}' at position ${position}`);
65
+ }
66
+ tokens.push({ kind: "end", value: "", position: source.length });
67
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
68
+ return tokens;
69
+ }
70
+ var Parser = class {
71
+ constructor(tokens, columns) {
72
+ this.tokens = tokens;
73
+ this.columns = /* @__PURE__ */ new Map();
74
+ for (const column of columns) {
75
+ for (const alias of [column.name, column.columnName]) {
76
+ const previous = this.columns.get(alias);
77
+ if (previous && previous.columnName !== column.columnName) {
78
+ throw new Error(`ambiguous check column '${alias}'`);
79
+ }
80
+ this.columns.set(alias, column);
81
+ }
82
+ }
83
+ }
84
+ index = 0;
85
+ depth = 0;
86
+ columns;
87
+ parse() {
88
+ const expression = this.parseOr();
89
+ this.expect("end", "end of expression");
90
+ return expression;
91
+ }
92
+ parseOr() {
93
+ let left = this.parseAnd();
94
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
95
+ return left;
96
+ }
97
+ parseAnd() {
98
+ let left = this.parseNot();
99
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
100
+ return left;
101
+ }
102
+ parseNot() {
103
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
104
+ return this.parsePrimary();
105
+ }
106
+ parsePrimary() {
107
+ if (this.peek().kind === "lparen") {
108
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
109
+ this.advance();
110
+ const expression = this.parseOr();
111
+ this.expect("rparen", "')'");
112
+ this.depth--;
113
+ return `(${expression})`;
114
+ }
115
+ return this.parseComparison();
116
+ }
117
+ parseComparison() {
118
+ const left = this.parseValue();
119
+ if (this.matchKeyword("is")) {
120
+ const negate = this.matchKeyword("not");
121
+ this.expectKeyword("null");
122
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
123
+ }
124
+ if (this.matchKeyword("between")) {
125
+ const lower = this.parseValue();
126
+ this.expectKeyword("and");
127
+ const upper = this.parseValue();
128
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
129
+ }
130
+ if (this.matchKeyword("in")) {
131
+ this.expect("lparen", "'(' after IN");
132
+ const values = [this.parseValue()];
133
+ while (this.peek().kind === "comma") {
134
+ this.advance();
135
+ values.push(this.parseValue());
136
+ }
137
+ this.expect("rparen", "')' after IN values");
138
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
139
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
140
+ }
141
+ const operator = this.expect("operator", "a comparison operator");
142
+ const right = this.parseValue();
143
+ if (operator.value === "~" || operator.value === "!~") {
144
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
145
+ if (!right.literal) throw new Error("regex pattern must be a literal");
146
+ }
147
+ return `${left.sql} ${operator.value} ${right.sql}`;
148
+ }
149
+ parseValue() {
150
+ const token = this.advance();
151
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
152
+ if (token.kind === "number") {
153
+ const number = Number(token.value);
154
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
155
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
156
+ }
157
+ return { sql: token.value, literal: true };
158
+ }
159
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
160
+ const keyword = token.value.toLowerCase();
161
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
162
+ if (keyword === "btrim") {
163
+ this.expect("lparen", "'(' after btrim");
164
+ const column2 = this.parseColumn();
165
+ this.expect("rparen", "')' after btrim column");
166
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
167
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
168
+ }
169
+ const column = this.columns.get(token.value);
170
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
171
+ return { sql: quoteIdentifier(column.columnName), column };
172
+ }
173
+ parseColumn() {
174
+ const token = this.expect("identifier", "a column name");
175
+ const column = this.columns.get(token.value);
176
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
177
+ return column;
178
+ }
179
+ peek() {
180
+ return this.tokens[this.index];
181
+ }
182
+ advance() {
183
+ const token = this.peek();
184
+ if (token.kind !== "end") this.index++;
185
+ return token;
186
+ }
187
+ matchKeyword(keyword) {
188
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
189
+ this.advance();
190
+ return true;
191
+ }
192
+ expectKeyword(keyword) {
193
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
194
+ }
195
+ expect(kind, description) {
196
+ const token = this.peek();
197
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
198
+ return this.advance();
199
+ }
200
+ };
201
+ function compileCheckExpression(source, columns) {
202
+ return new Parser(tokenize(source), columns).parse();
203
+ }
204
+
205
+ // src/constants.ts
206
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
207
+ var MAX_IDENTIFIER_LENGTH = 63;
208
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
209
+ var CELL_PART_SUFFIX = {
210
+ at: "__at",
211
+ by: "__by",
212
+ rev: "__rev",
213
+ source: "__src",
214
+ conflicts: "__lost"
215
+ };
216
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
217
+ function cellColumnName(column, part) {
218
+ return `${column}${CELL_PART_SUFFIX[part]}`;
219
+ }
220
+ var MATERIALIZE_ACTOR = "orm:materialize";
221
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
222
+ var policyVariantSchema = zod.z.object({
223
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
224
+ when: zod.z.string().min(1).max(1e3),
225
+ /** Exactly one of: allow-all bypass, or a row predicate. */
226
+ allow: zod.z.literal(true).optional(),
227
+ policy: zod.z.string().min(1).max(2e3).optional()
228
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
229
+ message: "A variant declares exactly one of allow: true or policy"
230
+ });
231
+ var policyExpressionSchema = zod.z.union([
232
+ zod.z.string().min(1).max(2e3),
233
+ zod.z.object({
234
+ default: zod.z.string().min(1).max(2e3),
235
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
236
+ })
237
+ ]);
238
+ function parseDurationMs(input) {
239
+ if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
240
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
241
+ if (!match) return void 0;
242
+ const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
243
+ const value = Math.round(Number(match[1]) * factor);
244
+ return value > 0 ? value : void 0;
245
+ }
246
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
247
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
248
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
249
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
250
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
251
+ };
252
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
253
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
254
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
255
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
256
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
257
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
258
+ };
259
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
260
+ var RESOLVED_SOURCE_CELL_PARTS = { manual: ["at", "by", "rev"], prefetch: ["at", "by"] };
261
+ var RESERVED_RESOLVED_COLUMN_NAMES = /* @__PURE__ */ new Set([
262
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
263
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
264
+ ]);
265
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
266
+ var canonicalFunctionRefSchema = zod.z.object({
267
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
268
+ kind: zod.z.enum(["managed", "native"]),
269
+ id: zod.z.string().min(1).max(200)
270
+ }).strict();
271
+ var canonicalFetchShapeSchema = zod.z.union([
272
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
273
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
274
+ ]);
275
+ var canonicalResolvedSourceSchema = zod.z.object({
276
+ name: identifier,
277
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
278
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
279
+ table: identifier,
280
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
281
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
282
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
283
+ updatedAt: zod.z.string().min(1).max(63).optional(),
284
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
285
+ key: zod.z.string().min(1).max(63).optional(),
286
+ fetch: canonicalFetchShapeSchema.optional(),
287
+ function: canonicalFunctionRefSchema.optional(),
288
+ prefetchDefaults: zod.z.object({
289
+ maxStalenessMs: durationMs,
290
+ timeoutMs: durationMs,
291
+ onError: zod.z.enum(["fallback", "fail"])
292
+ }).strict().optional(),
293
+ writeback: zod.z.object({
294
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
295
+ defaultMode: zod.z.enum(["sync", "async"]),
296
+ timeoutMs: durationMs,
297
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
298
+ clearManualOnConfirm: zod.z.boolean()
299
+ }).strict().optional()
300
+ }).strict();
301
+ var canonicalResolveRuleSchema = zod.z.object({
302
+ column: zod.z.string().min(1).max(63),
303
+ comparator: zod.z.enum(COMPARATORS),
304
+ /** Source names, in tie-break order. */
305
+ candidates: zod.z.array(identifier).min(1).max(16),
306
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
307
+ orderBy: zod.z.string().min(1).max(2e3).optional()
308
+ }).strict();
309
+ var canonicalResolvedSchema = zod.z.object({
310
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
311
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
312
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
313
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
314
+ deletedBy: zod.z.array(identifier).max(1).default([]),
315
+ /**
316
+ * Materialized: the view reads an indexed internal table (`table`) that the
317
+ * data plane keeps in step with `compute`, the internal view holding the
318
+ * resolution query. Omitted when the view computes on read.
319
+ */
320
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
321
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
322
+ policies: zod.z.object({
323
+ write: policyExpressionSchema.optional(),
324
+ insert: policyExpressionSchema.optional(),
325
+ update: policyExpressionSchema.optional()
326
+ }).strict().default({})
327
+ }).strict();
328
+ function validateResolvedView({ view, tables, views = [] }) {
329
+ const resolved = view.resolved;
330
+ if (!resolved) return [];
331
+ const errors = [];
332
+ const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
333
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
334
+ if (!pkColumn) {
335
+ fail("requires exactly one primary-key column");
336
+ return errors;
337
+ }
338
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
339
+ const scope = {
340
+ view,
341
+ resolved,
342
+ pkColumn,
343
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
344
+ sourceByName: /* @__PURE__ */ new Map(),
345
+ fail
346
+ };
347
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
348
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
349
+ for (const source of resolved.sources) {
350
+ if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
351
+ scope.sourceByName.set(source.name, source);
352
+ const table = tableByName.get(source.table);
353
+ if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
354
+ else validateSource({ scope, source, table });
355
+ }
356
+ validateRules(scope);
357
+ validateViewCells(scope);
358
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
359
+ for (const name of resolved.deletedBy) {
360
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
361
+ }
362
+ return errors;
363
+ }
364
+ function validateSource({ scope, source, table }) {
365
+ const { fail } = scope;
366
+ if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
367
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
368
+ fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
369
+ }
370
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
371
+ if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
372
+ if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
373
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
374
+ validateSourceColumns({ scope, source, byName });
375
+ if (source.kind === "etl") {
376
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
377
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
378
+ fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
379
+ }
380
+ }
381
+ validatePrefetchFields({ scope, source });
382
+ }
383
+ function validatePrefetchFields({ scope, source }) {
384
+ const { fail, pkColumn } = scope;
385
+ if (source.kind !== "prefetch") {
386
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
387
+ fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
388
+ }
389
+ return;
390
+ }
391
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
392
+ fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
393
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
394
+ fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
395
+ }
396
+ for (const column of source.writeback?.columns ?? []) {
397
+ if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
398
+ }
399
+ }
400
+ function validateSourceColumns({ scope, source, byName }) {
401
+ const { fail, pkColumn, dataColumns } = scope;
402
+ for (const column of [pkColumn.name, ...source.columns]) {
403
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
404
+ const theirs = byName.get(column);
405
+ if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
406
+ else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
407
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
408
+ }
409
+ }
410
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
411
+ const ruled = /* @__PURE__ */ new Set();
412
+ for (const rule of resolved.resolve) {
413
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
414
+ fail(`resolve rule targets unknown column '${rule.column}'`);
415
+ continue;
416
+ }
417
+ if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
418
+ ruled.add(rule.column);
419
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
420
+ fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
421
+ }
422
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
423
+ for (const candidate of rule.candidates) {
424
+ const source = sourceByName.get(candidate);
425
+ if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
426
+ else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
427
+ }
428
+ }
429
+ for (const column of dataColumns.keys()) {
430
+ if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
431
+ }
432
+ }
433
+ var VIEW_CELL_TYPES = {
434
+ source: "text",
435
+ at: "timestamptz",
436
+ by: "text",
437
+ conflicts: "jsonb"
438
+ };
439
+ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
440
+ const expected = /* @__PURE__ */ new Map();
441
+ for (const column of dataColumns.values()) {
442
+ if (column.name === pkColumn.name) continue;
443
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
444
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
445
+ }
446
+ }
447
+ for (const column of view.columns) {
448
+ if (!column.cell) continue;
449
+ const want = expected.get(column.name);
450
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
451
+ fail(`unexpected cell column '${column.name}'`);
452
+ continue;
453
+ }
454
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
455
+ expected.delete(column.name);
456
+ }
457
+ for (const name of expected.keys()) fail(`missing cell column '${name}'`);
458
+ }
459
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
460
+ function validateMaterialized({ scope, tableByName, views }) {
461
+ const { view, resolved, fail } = scope;
462
+ const { table: tableName, compute: computeName } = resolved.materialized;
463
+ const table = tableByName.get(tableName);
464
+ if (!table || table.internal !== true || table.materializes !== view.view) {
465
+ fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
466
+ } else {
467
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
468
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
469
+ }
470
+ const compute = views.find((candidate) => candidate.view === computeName);
471
+ if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
472
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
473
+ for (const source of resolved.sources) {
474
+ if (source.kind !== "etl") continue;
475
+ const read = tableByName.get(source.table)?.policies?.read;
476
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
477
+ if (!unconditional) {
478
+ fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
479
+ }
480
+ }
481
+ }
482
+
5
483
  // src/canonical/v2.ts
6
484
  var CANONICAL_FORMAT = 2;
7
485
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
8
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
486
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
487
+ var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
9
488
  var COLUMN_TYPES = [
10
489
  "uuid",
11
490
  "text",
@@ -20,59 +499,71 @@ var COLUMN_TYPES = [
20
499
  ];
21
500
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
22
501
  var canonicalEnumSchema = zod.z.object({
23
- name: identifier,
502
+ name: identifier2,
24
503
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
25
504
  });
26
505
  var canonicalColumnSchema = zod.z.object({
27
506
  /** Property name in TS (camelCase); columnName is the Postgres name. */
28
507
  name: zod.z.string().min(1).max(63),
29
- columnName: identifier,
30
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
508
+ columnName: columnIdentifier,
509
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
31
510
  nullable: zod.z.boolean().default(true),
32
511
  unique: zod.z.boolean().default(false),
33
512
  /** Literal default, or one of the allowlisted SQL functions. */
34
513
  default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
35
514
  defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
515
+ /**
516
+ * Stamp this column from the database clock on UPDATE (`now()`). The
517
+ * column stays insertable (defaults fill gaps) but is omitted from
518
+ * SetInput / settable so callers cannot backdate it.
519
+ */
520
+ onUpdateFn: zod.z.enum(["now()"]).optional(),
36
521
  description: zod.z.string().max(500).optional(),
37
522
  /** Migration hints — same semantics as v1. */
38
- renamedFrom: identifier.optional(),
523
+ renamedFrom: columnIdentifier.optional(),
39
524
  cast: zod.z.string().max(200).optional(),
40
- deprecated: zod.z.boolean().default(false)
525
+ deprecated: zod.z.boolean().default(false),
526
+ /**
527
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
528
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
529
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
530
+ * ordinary columns so their hashes are unchanged.
531
+ */
532
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
533
+ }).superRefine((column, ctx) => {
534
+ if (column.onUpdateFn === void 0) return;
535
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
536
+ ctx.addIssue({
537
+ code: "custom",
538
+ path: ["onUpdateFn"],
539
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
540
+ });
541
+ }
41
542
  });
42
543
  var canonicalLinkSchema = zod.z.object({
43
544
  name: zod.z.string().min(1).max(63),
44
545
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
45
546
  /** Target table name (namespace-local). Absent for toPlatformUser. */
46
- target: identifier.optional(),
547
+ target: identifier2.optional(),
47
548
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
48
- on: identifier,
549
+ on: columnIdentifier,
49
550
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
50
- references: identifier.optional(),
551
+ references: columnIdentifier.optional(),
51
552
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
52
553
  enforce: zod.z.boolean().default(false),
554
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
555
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
53
556
  description: zod.z.string().max(500).optional()
54
557
  });
55
558
  var canonicalIndexSchema = zod.z.object({
56
- columns: zod.z.array(identifier).min(1),
559
+ columns: zod.z.array(columnIdentifier).min(1),
57
560
  unique: zod.z.boolean().default(false),
58
- name: identifier.optional()
561
+ name: identifier2.optional()
59
562
  });
60
- var policyVariantSchema = zod.z.object({
61
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
62
- when: zod.z.string().min(1).max(1e3),
63
- /** Exactly one of: allow-all bypass, or a row predicate. */
64
- allow: zod.z.literal(true).optional(),
65
- policy: zod.z.string().min(1).max(2e3).optional()
66
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
67
- message: "A variant declares exactly one of allow: true or policy"
68
- });
69
- var policyExpressionSchema = zod.z.union([
70
- zod.z.string().min(1).max(2e3),
71
- zod.z.object({
72
- default: zod.z.string().min(1).max(2e3),
73
- variants: zod.z.array(policyVariantSchema).max(10).default([])
74
- })
75
- ]);
563
+ var canonicalCheckSchema = zod.z.object({
564
+ name: identifier2,
565
+ expression: zod.z.string().min(1).max(2e3)
566
+ }).strict();
76
567
  var canonicalPoliciesSchema = zod.z.object({
77
568
  read: policyExpressionSchema.optional(),
78
569
  write: policyExpressionSchema.optional(),
@@ -260,7 +751,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
260
751
  "width_bucket"
261
752
  ];
262
753
  var canonicalActionSchema = zod.z.object({
263
- name: identifier,
754
+ name: identifier2,
264
755
  requires: zod.z.string().max(1e3).optional(),
265
756
  params: zod.z.record(zod.z.string(), zod.z.object({
266
757
  type: columnTypeSchema,
@@ -272,9 +763,9 @@ var canonicalActionSchema = zod.z.object({
272
763
  });
273
764
  var canonicalCowMutationSchema = zod.z.object({
274
765
  kind: zod.z.enum(["override", "revert"]),
275
- name: identifier,
766
+ name: identifier2,
276
767
  /** Synced table (same namespace) the overlay copies from / falls back to. */
277
- synced: identifier,
768
+ synced: identifier2,
278
769
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
279
770
  pk: zod.z.string().min(1).max(63),
280
771
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -301,17 +792,28 @@ var canonicalCowMutationSchema = zod.z.object({
301
792
  }
302
793
  }
303
794
  });
795
+ var canonicalForeignKeySchema = zod.z.object({
796
+ name: identifier2,
797
+ columns: zod.z.array(columnIdentifier).min(1).max(8),
798
+ target: identifier2,
799
+ references: zod.z.array(columnIdentifier).min(1).max(8),
800
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
801
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
802
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
803
+ }).strict();
304
804
  var canonicalTableSchema = zod.z.object({
305
- table: identifier,
805
+ table: identifier2,
306
806
  description: zod.z.string().max(2e3).optional(),
307
807
  /** Table-level rename hint (new in v2). */
308
- renamedFrom: identifier.optional(),
808
+ renamedFrom: identifier2.optional(),
309
809
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
310
810
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
311
811
  columns: zod.z.array(canonicalColumnSchema).default([]),
312
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
812
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
313
813
  links: zod.z.array(canonicalLinkSchema).default([]),
314
814
  indexes: zod.z.array(canonicalIndexSchema).default([]),
815
+ /** Omitted by old definitions, preserving their existing content hashes. */
816
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
315
817
  policies: canonicalPoliciesSchema.default({}),
316
818
  /**
317
819
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -320,26 +822,64 @@ var canonicalTableSchema = zod.z.object({
320
822
  */
321
823
  actions: zod.z.array(canonicalActionSchema).default([]),
322
824
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
323
- cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
825
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
826
+ /** Omitted by old definitions, preserving their existing content hashes. */
827
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
828
+ /**
829
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
830
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
831
+ * on a resolved table's internal sources), listed explicitly in `columns`
832
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
833
+ * Omitted when off so existing definitions hash unchanged.
834
+ */
835
+ cellMetadata: zod.z.literal(true).optional(),
836
+ /**
837
+ * System-owned backing table of a resolved table (its manual or prefetch
838
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
839
+ * only the data plane writes them. Omitted when false.
840
+ */
841
+ internal: zod.z.literal(true).optional(),
842
+ /**
843
+ * The materialized copy of resolved table `materializes` (internal): the
844
+ * same columns as that view, including its cell parts, maintained by the
845
+ * data plane. Omitted otherwise.
846
+ */
847
+ materializes: identifier2.optional()
324
848
  });
849
+ var MAX_VIEW_QUERY = 1e5;
850
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
325
851
  var canonicalViewSchema = zod.z.object({
326
- view: identifier,
852
+ view: identifier2,
327
853
  description: zod.z.string().max(2e3).optional(),
328
854
  columns: zod.z.array(canonicalColumnSchema).min(1),
329
855
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
330
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
856
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
331
857
  links: zod.z.array(canonicalLinkSchema).default([]),
332
858
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
333
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
859
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
334
860
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
335
861
  }),
336
862
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
337
- dependencies: zod.z.array(identifier).max(500)
863
+ dependencies: zod.z.array(identifier2).max(500),
864
+ /**
865
+ * Present when this view is a resolved table (defineResolvedTable): the
866
+ * multi-source metadata behind its generated query. Omitted otherwise so
867
+ * existing views hash unchanged.
868
+ */
869
+ resolved: canonicalResolvedSchema.optional(),
870
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
871
+ internal: zod.z.literal(true).optional()
338
872
  }).superRefine((view, ctx) => {
873
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
874
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
875
+ }
339
876
  for (const [index, column] of view.columns.entries()) {
340
877
  if (column.default !== void 0 || column.defaultFn !== void 0) {
341
878
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
342
879
  }
880
+ if (column.onUpdateFn !== void 0) {
881
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
882
+ }
343
883
  if (column.unique) {
344
884
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
345
885
  }
@@ -347,15 +887,82 @@ var canonicalViewSchema = zod.z.object({
347
887
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
348
888
  }
349
889
  }
890
+ if (view.resolved === void 0 && view.internal !== true) {
891
+ for (const [index, column] of view.columns.entries()) {
892
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
893
+ }
894
+ }
350
895
  for (const [index, link] of view.links.entries()) {
351
896
  if (link.enforce) {
352
897
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
353
898
  }
899
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
900
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
901
+ }
354
902
  }
355
903
  });
904
+ var CELL_PART_TYPES = {
905
+ at: "timestamptz",
906
+ by: "text",
907
+ rev: "uuid",
908
+ source: "text",
909
+ conflicts: "jsonb"
910
+ };
911
+ function trackedCellColumns(table) {
912
+ const pk = new Set(table.primaryKey ?? []);
913
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
914
+ }
915
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
916
+ function validateCellMetadata(table, index, ctx) {
917
+ const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
918
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
919
+ if (table.materializes !== void 0) return;
920
+ if (table.cellMetadata !== true) {
921
+ for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
922
+ return;
923
+ }
924
+ if (table.source !== void 0) {
925
+ fail(`synced table '${table.table}' cannot track cell metadata`);
926
+ return;
927
+ }
928
+ const tracked = trackedCellColumns(table);
929
+ const byName = new Map(tracked.map((column) => [column.name, column]));
930
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
931
+ if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
932
+ const seen = /* @__PURE__ */ new Set();
933
+ for (const { column, position, cell } of cells) {
934
+ const path = ["tables", index, "columns", position];
935
+ const owner = byName.get(cell.of);
936
+ if (!owner) {
937
+ fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
938
+ continue;
939
+ }
940
+ 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);
941
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
942
+ fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
943
+ }
944
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
945
+ fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
946
+ }
947
+ const key = `${cell.of}:${cell.part}`;
948
+ if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
949
+ seen.add(key);
950
+ }
951
+ for (const column of tracked) {
952
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
953
+ fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
954
+ }
955
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
956
+ fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
957
+ }
958
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
959
+ if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
960
+ }
961
+ }
962
+ }
356
963
  var canonicalDefinitionSchema = zod.z.object({
357
964
  definition_format: zod.z.literal(CANONICAL_FORMAT),
358
- namespace: identifier.max(41),
965
+ namespace: identifier2.max(41),
359
966
  description: zod.z.string().max(2e3).optional(),
360
967
  enums: zod.z.array(canonicalEnumSchema).default([]),
361
968
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
@@ -373,6 +980,74 @@ var canonicalDefinitionSchema = zod.z.object({
373
980
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
374
981
  }
375
982
  seen.add(table.table);
983
+ validateCellMetadata(table, index, ctx);
984
+ if (table.source !== void 0) {
985
+ for (const [columnIndex, column] of table.columns.entries()) {
986
+ if (column.onUpdateFn !== void 0) {
987
+ ctx.addIssue({
988
+ code: "custom",
989
+ path: ["tables", index, "columns", columnIndex],
990
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
991
+ });
992
+ }
993
+ }
994
+ }
995
+ }
996
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
997
+ for (const [tableIndex, table] of definition.tables.entries()) {
998
+ const names = /* @__PURE__ */ new Set([
999
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
1000
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
1001
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
1002
+ ]);
1003
+ for (const [index, link] of table.links.entries()) {
1004
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
1005
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
1006
+ }
1007
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
1008
+ const source = table.columns.find((column) => column.columnName === link.on);
1009
+ if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "SET NULL requires a nullable source column" });
1010
+ }
1011
+ }
1012
+ for (const [index, check] of (table.checks ?? []).entries()) {
1013
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
1014
+ if (table.source) fail("synced tables cannot declare checks");
1015
+ if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
1016
+ names.add(check.name);
1017
+ try {
1018
+ compileCheckExpression(check.expression, table.columns);
1019
+ } catch (error) {
1020
+ fail(error instanceof Error ? error.message : String(error));
1021
+ }
1022
+ }
1023
+ for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
1024
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
1025
+ if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
1026
+ names.add(fk.name);
1027
+ const target = fkTables.get(fk.target);
1028
+ if (table.source || !target || target.source) {
1029
+ fail("foreign keys must connect owned tables in the same namespace");
1030
+ continue;
1031
+ }
1032
+ if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
1033
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
1034
+ for (const [position, name] of fk.columns.entries()) {
1035
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
1036
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
1037
+ if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
1038
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
1039
+ }
1040
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
1041
+ 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");
1042
+ }
1043
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
1044
+ 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");
1045
+ }
1046
+ }
1047
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
1048
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
1049
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
1050
+ }
376
1051
  }
377
1052
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
378
1053
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -497,8 +1172,12 @@ function canonicalRelations(definition) {
497
1172
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
498
1173
  links: view.links,
499
1174
  indexes: [],
500
- policies: { read: view.policies.read },
501
- actions: []
1175
+ // A resolved table's write policies (evaluated against the resolved row)
1176
+ // ride alongside the view's read policy.
1177
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
1178
+ actions: [],
1179
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
1180
+ ...view.internal ? { internal: true } : {}
502
1181
  }));
503
1182
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
504
1183
  }
@@ -508,12 +1187,14 @@ function normalizeCanonical(definition) {
508
1187
  ...definition,
509
1188
  enums: sortByName(definition.enums),
510
1189
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
511
- const { cowMutations, ...rest } = t;
1190
+ const { cowMutations, foreignKeys, checks, ...rest } = t;
512
1191
  return {
513
1192
  ...rest,
514
1193
  links: sortByName(t.links),
515
1194
  indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
516
1195
  actions: sortByName(t.actions),
1196
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1197
+ ...checks?.length ? { checks: sortByName(checks) } : {},
517
1198
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
518
1199
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
519
1200
  };
@@ -530,23 +1211,40 @@ function normalizeCanonical(definition) {
530
1211
  }
531
1212
 
532
1213
  exports.CANONICAL_FORMAT = CANONICAL_FORMAT;
1214
+ exports.CELL_PART_TYPES = CELL_PART_TYPES;
533
1215
  exports.COLUMN_TYPES = COLUMN_TYPES;
1216
+ exports.COMPARATORS = COMPARATORS;
534
1217
  exports.IDENTIFIER_RE = IDENTIFIER_RE;
1218
+ exports.RESERVED_RESOLVED_COLUMN_NAMES = RESERVED_RESOLVED_COLUMN_NAMES;
1219
+ exports.RESOLVED_SOURCE_CELL_PARTS = RESOLVED_SOURCE_CELL_PARTS;
1220
+ exports.RESOLVED_SOURCE_SYSTEM_COLUMNS = RESOLVED_SOURCE_SYSTEM_COLUMNS;
1221
+ exports.RESOLVED_VIEW_CELL_PARTS = RESOLVED_VIEW_CELL_PARTS;
1222
+ exports.RESOLVED_VIEW_SYSTEM_COLUMNS = RESOLVED_VIEW_SYSTEM_COLUMNS;
535
1223
  exports.VIEW_QUERY_FUNCTION_ALLOWLIST = VIEW_QUERY_FUNCTION_ALLOWLIST;
536
1224
  exports.canonicalActionSchema = canonicalActionSchema;
1225
+ exports.canonicalCheckSchema = canonicalCheckSchema;
537
1226
  exports.canonicalColumnSchema = canonicalColumnSchema;
538
1227
  exports.canonicalCowMutationSchema = canonicalCowMutationSchema;
539
1228
  exports.canonicalDefinitionSchema = canonicalDefinitionSchema;
540
1229
  exports.canonicalEnumSchema = canonicalEnumSchema;
1230
+ exports.canonicalFetchShapeSchema = canonicalFetchShapeSchema;
1231
+ exports.canonicalForeignKeySchema = canonicalForeignKeySchema;
1232
+ exports.canonicalFunctionRefSchema = canonicalFunctionRefSchema;
541
1233
  exports.canonicalIndexSchema = canonicalIndexSchema;
542
1234
  exports.canonicalLinkSchema = canonicalLinkSchema;
543
1235
  exports.canonicalPoliciesSchema = canonicalPoliciesSchema;
544
1236
  exports.canonicalRelations = canonicalRelations;
1237
+ exports.canonicalResolveRuleSchema = canonicalResolveRuleSchema;
1238
+ exports.canonicalResolvedSchema = canonicalResolvedSchema;
1239
+ exports.canonicalResolvedSourceSchema = canonicalResolvedSourceSchema;
545
1240
  exports.canonicalTableSchema = canonicalTableSchema;
546
1241
  exports.canonicalViewSchema = canonicalViewSchema;
547
1242
  exports.isSingleViewQuerySql = isSingleViewQuerySql;
548
1243
  exports.normalizeCanonical = normalizeCanonical;
1244
+ exports.parseDurationMs = parseDurationMs;
549
1245
  exports.policyExpressionSchema = policyExpressionSchema;
550
1246
  exports.policyVariantSchema = policyVariantSchema;
1247
+ exports.trackedCellColumns = trackedCellColumns;
1248
+ exports.validateResolvedView = validateResolvedView;
551
1249
  //# sourceMappingURL=canonical.cjs.map
552
1250
  //# sourceMappingURL=canonical.cjs.map