@sequenceholdings/orm 2.1.0 → 2.1.1

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