@sequenceholdings/orm 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +94 -2
  2. package/dist/authoring.cjs +1474 -54
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +242 -4
  5. package/dist/authoring.d.ts +242 -4
  6. package/dist/authoring.js +1465 -56
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +740 -42
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +742 -53
  11. package/dist/canonical.d.ts +742 -53
  12. package/dist/canonical.js +724 -43
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +2478 -328
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +2480 -330
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs +90 -25
  19. package/dist/client.cjs.map +1 -1
  20. package/dist/client.d.cts +39 -22
  21. package/dist/client.d.ts +39 -22
  22. package/dist/client.js +87 -26
  23. package/dist/client.js.map +1 -1
  24. package/dist/compiler.cjs +1755 -329
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +56 -3
  27. package/dist/compiler.d.ts +56 -3
  28. package/dist/compiler.js +1756 -330
  29. package/dist/compiler.js.map +1 -1
  30. package/dist/constants-YoFGjIBM.d.cts +99 -0
  31. package/dist/constants-YoFGjIBM.d.ts +99 -0
  32. package/dist/dynamic-input.d.cts +1 -0
  33. package/dist/dynamic-input.d.ts +1 -0
  34. package/dist/index.cjs +1973 -164
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +474 -100
  37. package/dist/index.d.ts +474 -100
  38. package/dist/index.js +1940 -167
  39. package/dist/index.js.map +1 -1
  40. package/dist/ir.cjs +869 -46
  41. package/dist/ir.cjs.map +1 -1
  42. package/dist/ir.d.cts +699 -25
  43. package/dist/ir.d.ts +699 -25
  44. package/dist/ir.js +861 -47
  45. package/dist/ir.js.map +1 -1
  46. package/dist/{naming-CTvWSRTf.d.ts → naming-CPfx7EfF.d.ts} +28 -1
  47. package/dist/{naming-DRyinU4u.d.cts → naming-CU9ThP_E.d.cts} +28 -1
  48. package/dist/policy-1iZbGnZZ.d.cts +24 -0
  49. package/dist/policy-1iZbGnZZ.d.ts +24 -0
  50. package/dist/predicate-v2.cjs +852 -49
  51. package/dist/predicate-v2.cjs.map +1 -1
  52. package/dist/predicate-v2.d.cts +2 -1
  53. package/dist/predicate-v2.d.ts +2 -1
  54. package/dist/predicate-v2.js +852 -49
  55. package/dist/predicate-v2.js.map +1 -1
  56. package/dist/sdl.cjs +1309 -58
  57. package/dist/sdl.cjs.map +1 -1
  58. package/dist/sdl.d.cts +7 -3
  59. package/dist/sdl.d.ts +7 -3
  60. package/dist/sdl.js +1305 -59
  61. package/dist/sdl.js.map +1 -1
  62. package/package.json +11 -6
  63. package/templates/orm-namespace/CLAUDE.md +6 -4
  64. package/templates/orm-namespace/codegen.preset.d.cts +7 -0
  65. package/templates/orm-namespace/codegen.ts +2 -1
  66. package/templates/orm-namespace/graphql/example/queries.ts +4 -3
  67. package/templates/orm-namespace/package.json +2 -1
  68. package/templates/orm-namespace/schema/tables.ts +3 -2
  69. package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/ir.cjs CHANGED
@@ -3,9 +3,480 @@
3
3
  var zod = require('zod');
4
4
 
5
5
  // src/ir/index.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
+ var CELLS_FIELD = "cells";
218
+ function cellColumnName(column, part) {
219
+ return `${column}${CELL_PART_SUFFIX[part]}`;
220
+ }
221
+ var MATERIALIZE_ACTOR = "orm:materialize";
222
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
223
+ var policyVariantSchema = zod.z.object({
224
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
225
+ when: zod.z.string().min(1).max(1e3),
226
+ /** Exactly one of: allow-all bypass, or a row predicate. */
227
+ allow: zod.z.literal(true).optional(),
228
+ policy: zod.z.string().min(1).max(2e3).optional()
229
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
230
+ message: "A variant declares exactly one of allow: true or policy"
231
+ });
232
+ var policyExpressionSchema = zod.z.union([
233
+ zod.z.string().min(1).max(2e3),
234
+ zod.z.object({
235
+ default: zod.z.string().min(1).max(2e3),
236
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
237
+ })
238
+ ]);
239
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
240
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
241
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
242
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
243
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
244
+ };
245
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
246
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
247
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
248
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
249
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
250
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
251
+ };
252
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
253
+ /* @__PURE__ */ new Set([
254
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
255
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
256
+ ]);
257
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
258
+ var canonicalFunctionRefSchema = zod.z.object({
259
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
260
+ kind: zod.z.enum(["managed", "native"]),
261
+ id: zod.z.string().min(1).max(200)
262
+ }).strict();
263
+ var canonicalFetchShapeSchema = zod.z.union([
264
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
265
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
266
+ ]);
267
+ var canonicalResolvedSourceSchema = zod.z.object({
268
+ name: identifier,
269
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
270
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
271
+ table: identifier,
272
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
273
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
274
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
275
+ updatedAt: zod.z.string().min(1).max(63).optional(),
276
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
277
+ key: zod.z.string().min(1).max(63).optional(),
278
+ fetch: canonicalFetchShapeSchema.optional(),
279
+ function: canonicalFunctionRefSchema.optional(),
280
+ prefetchDefaults: zod.z.object({
281
+ maxStalenessMs: durationMs,
282
+ timeoutMs: durationMs,
283
+ onError: zod.z.enum(["fallback", "fail"])
284
+ }).strict().optional(),
285
+ writeback: zod.z.object({
286
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
287
+ defaultMode: zod.z.enum(["sync", "async"]),
288
+ timeoutMs: durationMs,
289
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
290
+ clearManualOnConfirm: zod.z.boolean()
291
+ }).strict().optional()
292
+ }).strict();
293
+ var canonicalResolveRuleSchema = zod.z.object({
294
+ column: zod.z.string().min(1).max(63),
295
+ comparator: zod.z.enum(COMPARATORS),
296
+ /** Source names, in tie-break order. */
297
+ candidates: zod.z.array(identifier).min(1).max(16),
298
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
299
+ orderBy: zod.z.string().min(1).max(2e3).optional()
300
+ }).strict();
301
+ var canonicalResolvedSchema = zod.z.object({
302
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
303
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
304
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
305
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
306
+ deletedBy: zod.z.array(identifier).max(1).default([]),
307
+ /**
308
+ * Materialized: the view reads an indexed internal table (`table`) that the
309
+ * data plane keeps in step with `compute`, the internal view holding the
310
+ * resolution query. Omitted when the view computes on read.
311
+ */
312
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
313
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
314
+ policies: zod.z.object({
315
+ write: policyExpressionSchema.optional(),
316
+ insert: policyExpressionSchema.optional(),
317
+ update: policyExpressionSchema.optional()
318
+ }).strict().default({})
319
+ }).strict();
320
+ function validateResolvedView({ view, tables, views = [] }) {
321
+ const resolved = view.resolved;
322
+ if (!resolved) return [];
323
+ const errors = [];
324
+ const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
325
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
326
+ if (!pkColumn) {
327
+ fail("requires exactly one primary-key column");
328
+ return errors;
329
+ }
330
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
331
+ const scope = {
332
+ view,
333
+ resolved,
334
+ pkColumn,
335
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
336
+ sourceByName: /* @__PURE__ */ new Map(),
337
+ fail
338
+ };
339
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
340
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
341
+ for (const source of resolved.sources) {
342
+ if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
343
+ scope.sourceByName.set(source.name, source);
344
+ const table = tableByName.get(source.table);
345
+ if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
346
+ else validateSource({ scope, source, table });
347
+ }
348
+ validateRules(scope);
349
+ validateViewCells(scope);
350
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
351
+ for (const name of resolved.deletedBy) {
352
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
353
+ }
354
+ return errors;
355
+ }
356
+ function validateSource({ scope, source, table }) {
357
+ const { fail } = scope;
358
+ if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
359
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
360
+ fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
361
+ }
362
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
363
+ if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
364
+ if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
365
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
366
+ validateSourceColumns({ scope, source, byName });
367
+ if (source.kind === "etl") {
368
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
369
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
370
+ fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
371
+ }
372
+ }
373
+ validatePrefetchFields({ scope, source });
374
+ }
375
+ function validatePrefetchFields({ scope, source }) {
376
+ const { fail, pkColumn } = scope;
377
+ if (source.kind !== "prefetch") {
378
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
379
+ fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
380
+ }
381
+ return;
382
+ }
383
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
384
+ fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
385
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
386
+ fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
387
+ }
388
+ for (const column of source.writeback?.columns ?? []) {
389
+ if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
390
+ }
391
+ }
392
+ function validateSourceColumns({ scope, source, byName }) {
393
+ const { fail, pkColumn, dataColumns } = scope;
394
+ for (const column of [pkColumn.name, ...source.columns]) {
395
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
396
+ const theirs = byName.get(column);
397
+ if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
398
+ else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
399
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
400
+ }
401
+ }
402
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
403
+ const ruled = /* @__PURE__ */ new Set();
404
+ for (const rule of resolved.resolve) {
405
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
406
+ fail(`resolve rule targets unknown column '${rule.column}'`);
407
+ continue;
408
+ }
409
+ if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
410
+ ruled.add(rule.column);
411
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
412
+ fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
413
+ }
414
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
415
+ for (const candidate of rule.candidates) {
416
+ const source = sourceByName.get(candidate);
417
+ if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
418
+ else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
419
+ }
420
+ }
421
+ for (const column of dataColumns.keys()) {
422
+ if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
423
+ }
424
+ }
425
+ var VIEW_CELL_TYPES = {
426
+ source: "text",
427
+ at: "timestamptz",
428
+ by: "text",
429
+ conflicts: "jsonb"
430
+ };
431
+ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
432
+ const expected = /* @__PURE__ */ new Map();
433
+ for (const column of dataColumns.values()) {
434
+ if (column.name === pkColumn.name) continue;
435
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
436
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
437
+ }
438
+ }
439
+ for (const column of view.columns) {
440
+ if (!column.cell) continue;
441
+ const want = expected.get(column.name);
442
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
443
+ fail(`unexpected cell column '${column.name}'`);
444
+ continue;
445
+ }
446
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
447
+ expected.delete(column.name);
448
+ }
449
+ for (const name of expected.keys()) fail(`missing cell column '${name}'`);
450
+ }
451
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
452
+ function validateMaterialized({ scope, tableByName, views }) {
453
+ const { view, resolved, fail } = scope;
454
+ const { table: tableName, compute: computeName } = resolved.materialized;
455
+ const table = tableByName.get(tableName);
456
+ if (!table || table.internal !== true || table.materializes !== view.view) {
457
+ fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
458
+ } else {
459
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
460
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
461
+ }
462
+ const compute = views.find((candidate) => candidate.view === computeName);
463
+ if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
464
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
465
+ for (const source of resolved.sources) {
466
+ if (source.kind !== "etl") continue;
467
+ const read = tableByName.get(source.table)?.policies?.read;
468
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
469
+ if (!unconditional) {
470
+ fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
471
+ }
472
+ }
473
+ }
474
+
475
+ // src/canonical/v2.ts
6
476
  var CANONICAL_FORMAT = 2;
7
477
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
8
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
478
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
479
+ var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
9
480
  var COLUMN_TYPES = [
10
481
  "uuid",
11
482
  "text",
@@ -20,59 +491,71 @@ var COLUMN_TYPES = [
20
491
  ];
21
492
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
22
493
  var canonicalEnumSchema = zod.z.object({
23
- name: identifier,
494
+ name: identifier2,
24
495
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
25
496
  });
26
497
  var canonicalColumnSchema = zod.z.object({
27
498
  /** Property name in TS (camelCase); columnName is the Postgres name. */
28
499
  name: zod.z.string().min(1).max(63),
29
- columnName: identifier,
30
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
500
+ columnName: columnIdentifier,
501
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
31
502
  nullable: zod.z.boolean().default(true),
32
503
  unique: zod.z.boolean().default(false),
33
504
  /** Literal default, or one of the allowlisted SQL functions. */
34
505
  default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
35
506
  defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
507
+ /**
508
+ * Stamp this column from the database clock on UPDATE (`now()`). The
509
+ * column stays insertable (defaults fill gaps) but is omitted from
510
+ * SetInput / settable so callers cannot backdate it.
511
+ */
512
+ onUpdateFn: zod.z.enum(["now()"]).optional(),
36
513
  description: zod.z.string().max(500).optional(),
37
514
  /** Migration hints — same semantics as v1. */
38
- renamedFrom: identifier.optional(),
515
+ renamedFrom: columnIdentifier.optional(),
39
516
  cast: zod.z.string().max(200).optional(),
40
- deprecated: zod.z.boolean().default(false)
517
+ deprecated: zod.z.boolean().default(false),
518
+ /**
519
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
520
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
521
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
522
+ * ordinary columns so their hashes are unchanged.
523
+ */
524
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
525
+ }).superRefine((column, ctx) => {
526
+ if (column.onUpdateFn === void 0) return;
527
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
528
+ ctx.addIssue({
529
+ code: "custom",
530
+ path: ["onUpdateFn"],
531
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
532
+ });
533
+ }
41
534
  });
42
535
  var canonicalLinkSchema = zod.z.object({
43
536
  name: zod.z.string().min(1).max(63),
44
537
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
45
538
  /** Target table name (namespace-local). Absent for toPlatformUser. */
46
- target: identifier.optional(),
539
+ target: identifier2.optional(),
47
540
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
48
- on: identifier,
541
+ on: columnIdentifier,
49
542
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
50
- references: identifier.optional(),
543
+ references: columnIdentifier.optional(),
51
544
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
52
545
  enforce: zod.z.boolean().default(false),
546
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
547
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
53
548
  description: zod.z.string().max(500).optional()
54
549
  });
55
550
  var canonicalIndexSchema = zod.z.object({
56
- columns: zod.z.array(identifier).min(1),
551
+ columns: zod.z.array(columnIdentifier).min(1),
57
552
  unique: zod.z.boolean().default(false),
58
- name: identifier.optional()
59
- });
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"
553
+ name: identifier2.optional()
68
554
  });
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
- ]);
555
+ var canonicalCheckSchema = zod.z.object({
556
+ name: identifier2,
557
+ expression: zod.z.string().min(1).max(2e3)
558
+ }).strict();
76
559
  var canonicalPoliciesSchema = zod.z.object({
77
560
  read: policyExpressionSchema.optional(),
78
561
  write: policyExpressionSchema.optional(),
@@ -103,7 +586,7 @@ function isSingleViewQuerySql(value) {
103
586
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
104
587
  }
105
588
  var canonicalActionSchema = zod.z.object({
106
- name: identifier,
589
+ name: identifier2,
107
590
  requires: zod.z.string().max(1e3).optional(),
108
591
  params: zod.z.record(zod.z.string(), zod.z.object({
109
592
  type: columnTypeSchema,
@@ -115,9 +598,9 @@ var canonicalActionSchema = zod.z.object({
115
598
  });
116
599
  var canonicalCowMutationSchema = zod.z.object({
117
600
  kind: zod.z.enum(["override", "revert"]),
118
- name: identifier,
601
+ name: identifier2,
119
602
  /** Synced table (same namespace) the overlay copies from / falls back to. */
120
- synced: identifier,
603
+ synced: identifier2,
121
604
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
122
605
  pk: zod.z.string().min(1).max(63),
123
606
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -144,17 +627,28 @@ var canonicalCowMutationSchema = zod.z.object({
144
627
  }
145
628
  }
146
629
  });
630
+ var canonicalForeignKeySchema = zod.z.object({
631
+ name: identifier2,
632
+ columns: zod.z.array(columnIdentifier).min(1).max(8),
633
+ target: identifier2,
634
+ references: zod.z.array(columnIdentifier).min(1).max(8),
635
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
636
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
637
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
638
+ }).strict();
147
639
  var canonicalTableSchema = zod.z.object({
148
- table: identifier,
640
+ table: identifier2,
149
641
  description: zod.z.string().max(2e3).optional(),
150
642
  /** Table-level rename hint (new in v2). */
151
- renamedFrom: identifier.optional(),
643
+ renamedFrom: identifier2.optional(),
152
644
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
153
645
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
154
646
  columns: zod.z.array(canonicalColumnSchema).default([]),
155
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
647
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
156
648
  links: zod.z.array(canonicalLinkSchema).default([]),
157
649
  indexes: zod.z.array(canonicalIndexSchema).default([]),
650
+ /** Omitted by old definitions, preserving their existing content hashes. */
651
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
158
652
  policies: canonicalPoliciesSchema.default({}),
159
653
  /**
160
654
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -163,26 +657,64 @@ var canonicalTableSchema = zod.z.object({
163
657
  */
164
658
  actions: zod.z.array(canonicalActionSchema).default([]),
165
659
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
166
- cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
660
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
661
+ /** Omitted by old definitions, preserving their existing content hashes. */
662
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
663
+ /**
664
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
665
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
666
+ * on a resolved table's internal sources), listed explicitly in `columns`
667
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
668
+ * Omitted when off so existing definitions hash unchanged.
669
+ */
670
+ cellMetadata: zod.z.literal(true).optional(),
671
+ /**
672
+ * System-owned backing table of a resolved table (its manual or prefetch
673
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
674
+ * only the data plane writes them. Omitted when false.
675
+ */
676
+ internal: zod.z.literal(true).optional(),
677
+ /**
678
+ * The materialized copy of resolved table `materializes` (internal): the
679
+ * same columns as that view, including its cell parts, maintained by the
680
+ * data plane. Omitted otherwise.
681
+ */
682
+ materializes: identifier2.optional()
167
683
  });
684
+ var MAX_VIEW_QUERY = 1e5;
685
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
168
686
  var canonicalViewSchema = zod.z.object({
169
- view: identifier,
687
+ view: identifier2,
170
688
  description: zod.z.string().max(2e3).optional(),
171
689
  columns: zod.z.array(canonicalColumnSchema).min(1),
172
690
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
173
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
691
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
174
692
  links: zod.z.array(canonicalLinkSchema).default([]),
175
693
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
176
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
694
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
177
695
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
178
696
  }),
179
697
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
180
- dependencies: zod.z.array(identifier).max(500)
698
+ dependencies: zod.z.array(identifier2).max(500),
699
+ /**
700
+ * Present when this view is a resolved table (defineResolvedTable): the
701
+ * multi-source metadata behind its generated query. Omitted otherwise so
702
+ * existing views hash unchanged.
703
+ */
704
+ resolved: canonicalResolvedSchema.optional(),
705
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
706
+ internal: zod.z.literal(true).optional()
181
707
  }).superRefine((view, ctx) => {
708
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
709
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
710
+ }
182
711
  for (const [index, column] of view.columns.entries()) {
183
712
  if (column.default !== void 0 || column.defaultFn !== void 0) {
184
713
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
185
714
  }
715
+ if (column.onUpdateFn !== void 0) {
716
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
717
+ }
186
718
  if (column.unique) {
187
719
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
188
720
  }
@@ -190,15 +722,82 @@ var canonicalViewSchema = zod.z.object({
190
722
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
191
723
  }
192
724
  }
725
+ if (view.resolved === void 0 && view.internal !== true) {
726
+ for (const [index, column] of view.columns.entries()) {
727
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
728
+ }
729
+ }
193
730
  for (const [index, link] of view.links.entries()) {
194
731
  if (link.enforce) {
195
732
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
196
733
  }
734
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
735
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
736
+ }
197
737
  }
198
738
  });
739
+ var CELL_PART_TYPES = {
740
+ at: "timestamptz",
741
+ by: "text",
742
+ rev: "uuid",
743
+ source: "text",
744
+ conflicts: "jsonb"
745
+ };
746
+ function trackedCellColumns(table) {
747
+ const pk = new Set(table.primaryKey ?? []);
748
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
749
+ }
750
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
751
+ function validateCellMetadata(table, index, ctx) {
752
+ const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
753
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
754
+ if (table.materializes !== void 0) return;
755
+ if (table.cellMetadata !== true) {
756
+ for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
757
+ return;
758
+ }
759
+ if (table.source !== void 0) {
760
+ fail(`synced table '${table.table}' cannot track cell metadata`);
761
+ return;
762
+ }
763
+ const tracked = trackedCellColumns(table);
764
+ const byName = new Map(tracked.map((column) => [column.name, column]));
765
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
766
+ if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
767
+ const seen = /* @__PURE__ */ new Set();
768
+ for (const { column, position, cell } of cells) {
769
+ const path = ["tables", index, "columns", position];
770
+ const owner = byName.get(cell.of);
771
+ if (!owner) {
772
+ fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
773
+ continue;
774
+ }
775
+ 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);
776
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
777
+ fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
778
+ }
779
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
780
+ fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
781
+ }
782
+ const key = `${cell.of}:${cell.part}`;
783
+ if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
784
+ seen.add(key);
785
+ }
786
+ for (const column of tracked) {
787
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
788
+ fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
789
+ }
790
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
791
+ fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
792
+ }
793
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
794
+ if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
795
+ }
796
+ }
797
+ }
199
798
  zod.z.object({
200
799
  definition_format: zod.z.literal(CANONICAL_FORMAT),
201
- namespace: identifier.max(41),
800
+ namespace: identifier2.max(41),
202
801
  description: zod.z.string().max(2e3).optional(),
203
802
  enums: zod.z.array(canonicalEnumSchema).default([]),
204
803
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
@@ -216,6 +815,74 @@ zod.z.object({
216
815
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
217
816
  }
218
817
  seen.add(table.table);
818
+ validateCellMetadata(table, index, ctx);
819
+ if (table.source !== void 0) {
820
+ for (const [columnIndex, column] of table.columns.entries()) {
821
+ if (column.onUpdateFn !== void 0) {
822
+ ctx.addIssue({
823
+ code: "custom",
824
+ path: ["tables", index, "columns", columnIndex],
825
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
826
+ });
827
+ }
828
+ }
829
+ }
830
+ }
831
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
832
+ for (const [tableIndex, table] of definition.tables.entries()) {
833
+ const names = /* @__PURE__ */ new Set([
834
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
835
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
836
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
837
+ ]);
838
+ for (const [index, link] of table.links.entries()) {
839
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
840
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
841
+ }
842
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
843
+ const source = table.columns.find((column) => column.columnName === link.on);
844
+ 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" });
845
+ }
846
+ }
847
+ for (const [index, check] of (table.checks ?? []).entries()) {
848
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
849
+ if (table.source) fail("synced tables cannot declare checks");
850
+ if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
851
+ names.add(check.name);
852
+ try {
853
+ compileCheckExpression(check.expression, table.columns);
854
+ } catch (error) {
855
+ fail(error instanceof Error ? error.message : String(error));
856
+ }
857
+ }
858
+ for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
859
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
860
+ if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
861
+ names.add(fk.name);
862
+ const target = fkTables.get(fk.target);
863
+ if (table.source || !target || target.source) {
864
+ fail("foreign keys must connect owned tables in the same namespace");
865
+ continue;
866
+ }
867
+ if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
868
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
869
+ for (const [position, name] of fk.columns.entries()) {
870
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
871
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
872
+ if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
873
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
874
+ }
875
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
876
+ 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");
877
+ }
878
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
879
+ 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");
880
+ }
881
+ }
882
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
883
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
884
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
885
+ }
219
886
  }
220
887
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
221
888
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -334,7 +1001,18 @@ zod.z.object({
334
1001
  });
335
1002
 
336
1003
  // src/ir/index.ts
337
- var IR_VERSION = 1;
1004
+ var IR_VERSION = 2;
1005
+ var LEGACY_IR_VERSION = 1;
1006
+ var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
1007
+ var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
1008
+ function requiredArtifactIrVersion(operations) {
1009
+ let required = LEGACY_IR_VERSION;
1010
+ for (const operation of operations) {
1011
+ const { irVersion } = operationVersionSchema.parse(operation);
1012
+ if (irVersion > required) required = irVersion;
1013
+ }
1014
+ return required;
1015
+ }
338
1016
  var CLAIMS_CONTRACT = {
339
1017
  sub: "string",
340
1018
  org: "string?",
@@ -406,6 +1084,21 @@ var selectionIrSchema = zod.z.lazy(
406
1084
  () => zod.z.object({
407
1085
  typename: zod.z.string(),
408
1086
  columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
1087
+ cells: zod.z.object({
1088
+ alias: zod.z.string(),
1089
+ typename: zod.z.string(),
1090
+ fields: zod.z.array(zod.z.object({
1091
+ alias: zod.z.string(),
1092
+ column: zod.z.string(),
1093
+ typename: zod.z.string(),
1094
+ parts: zod.z.array(zod.z.object({
1095
+ alias: zod.z.string(),
1096
+ part: zod.z.enum(["source", "at", "by", "conflicts"]),
1097
+ fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
1098
+ typename: zod.z.string().optional()
1099
+ }))
1100
+ }))
1101
+ }).optional(),
409
1102
  relations: zod.z.array(zod.z.object({
410
1103
  alias: zod.z.string(),
411
1104
  link: zod.z.string(),
@@ -427,6 +1120,11 @@ var selectionIrSchema = zod.z.lazy(
427
1120
  })
428
1121
  );
429
1122
  var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
1123
+ var prefetchPlanIrSchema = zod.z.object({
1124
+ source: zod.z.string(),
1125
+ maxStalenessMs: zod.z.number().int().positive().optional(),
1126
+ key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
1127
+ });
430
1128
  var selectRootSchema = zod.z.object({
431
1129
  kind: zod.z.literal("select"),
432
1130
  alias: zod.z.string(),
@@ -438,7 +1136,8 @@ var selectRootSchema = zod.z.object({
438
1136
  distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
439
1137
  limit: limitIrSchema,
440
1138
  offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
441
- includeIf: includeIfSchema.optional()
1139
+ includeIf: includeIfSchema.optional(),
1140
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
442
1141
  });
443
1142
  var byPkRootSchema = zod.z.object({
444
1143
  kind: zod.z.literal("byPk"),
@@ -448,7 +1147,8 @@ var byPkRootSchema = zod.z.object({
448
1147
  pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
449
1148
  /** Policy predicate — ANDed with the PK match. */
450
1149
  where: predIrSchema,
451
- includeIf: includeIfSchema.optional()
1150
+ includeIf: includeIfSchema.optional(),
1151
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
452
1152
  });
453
1153
  var aggregateRootSchema = zod.z.object({
454
1154
  kind: zod.z.literal("aggregate"),
@@ -461,6 +1161,33 @@ var aggregateRootSchema = zod.z.object({
461
1161
  nodes: selectionIrSchema.optional(),
462
1162
  includeIf: includeIfSchema.optional()
463
1163
  });
1164
+ var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
1165
+ var groupByKeyIrSchema = zod.z.object({
1166
+ column: zod.z.string(),
1167
+ bucket: zod.z.enum(DATE_BUCKETS).optional()
1168
+ });
1169
+ var aggregateGroupsRootSchema = zod.z.object({
1170
+ kind: zod.z.literal("aggregateGroups"),
1171
+ alias: zod.z.string(),
1172
+ table: zod.z.string(),
1173
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
1174
+ where: predIrSchema,
1175
+ groupBy: zod.z.array(groupByKeyIrSchema).min(1),
1176
+ /**
1177
+ * The `key { … }` projection: which group columns appear in each entry's key
1178
+ * object, in selection order. A subset of `groupBy` (the compiler rejects a
1179
+ * key field that is not a group key — it would be indeterminate within the
1180
+ * group). Empty when the document selects only `aggregate`.
1181
+ */
1182
+ key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
1183
+ count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
1184
+ sum: zod.z.array(zod.z.string()).optional(),
1185
+ avg: zod.z.array(zod.z.string()).optional(),
1186
+ /** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
1187
+ limit: limitIrSchema,
1188
+ offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
1189
+ includeIf: includeIfSchema.optional()
1190
+ });
464
1191
  var writeValuesSchema = zod.z.union([
465
1192
  zod.z.object({ slot: zod.z.string() }),
466
1193
  zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
@@ -479,7 +1206,20 @@ var mutationRootSchema = zod.z.object({
479
1206
  * coalesce view falls back to synced. Both are gated on a matching synced
480
1207
  * row and carry the write policy in `where` like update/delete.
481
1208
  */
482
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
1209
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
1210
+ /**
1211
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
1212
+ * executor gates on the resolved row (write policy in `where`), writes only
1213
+ * the touched cells into the manual source, then re-checks `insertCheck`
1214
+ * against the resolved row. `revert` clears `revertColumns` from the manual
1215
+ * source; `delete` tombstones the row there.
1216
+ */
1217
+ resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
1218
+ revertColumns: zod.z.array(zod.z.string()).optional(),
1219
+ /** `@writeback(source:, mode:)` directives on this root. */
1220
+ writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
1221
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
1222
+ localOnly: zod.z.array(zod.z.string()).optional(),
483
1223
  alias: zod.z.string(),
484
1224
  table: zod.z.string(),
485
1225
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -506,12 +1246,15 @@ var mutationRootSchema = zod.z.object({
506
1246
  insertCheck: predIrSchema.optional(),
507
1247
  returning: selectionIrSchema.nullable(),
508
1248
  affectedRows: zod.z.boolean().default(false),
1249
+ /** v2: abort the transaction when the actual write count differs, regardless of selection. */
1250
+ assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
509
1251
  includeIf: includeIfSchema.optional()
510
1252
  });
511
1253
  var rootIrSchema = zod.z.discriminatedUnion("kind", [
512
1254
  selectRootSchema,
513
1255
  byPkRootSchema,
514
1256
  aggregateRootSchema,
1257
+ aggregateGroupsRootSchema,
515
1258
  mutationRootSchema
516
1259
  ]);
517
1260
  var INPUT_REF_KINDS = [
@@ -541,7 +1284,7 @@ var variableIrSchema = zod.z.object({
541
1284
  defaultValue: variableDefaultIrSchema.optional()
542
1285
  });
543
1286
  var compiledOperationSchema = zod.z.object({
544
- irVersion: zod.z.literal(IR_VERSION),
1287
+ irVersion: irVersionSchema,
545
1288
  name: zod.z.string(),
546
1289
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
547
1290
  kind: zod.z.enum(["query", "mutation"]),
@@ -553,6 +1296,50 @@ var compiledOperationSchema = zod.z.object({
553
1296
  /** Every table the op touches — data-token scoping + audit. */
554
1297
  tableSet: zod.z.array(zod.z.string()).min(1),
555
1298
  roots: zod.z.array(rootIrSchema).min(1)
1299
+ }).superRefine((operation, ctx) => {
1300
+ for (const [index, root] of operation.roots.entries()) {
1301
+ if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
1302
+ if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
1303
+ ctx.addIssue({ code: "custom", path: ["roots", index], message: "affected-row assertions require unconditional mutation roots and IR v2" });
1304
+ }
1305
+ }
1306
+ });
1307
+ var resolvedSourceIrSchema = zod.z.object({
1308
+ name: zod.z.string(),
1309
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
1310
+ /** Logical table key of the backing relation. */
1311
+ table: zod.z.string(),
1312
+ columns: zod.z.array(zod.z.string()),
1313
+ key: zod.z.string().optional(),
1314
+ fetch: zod.z.union([
1315
+ zod.z.object({ shape: zod.z.literal("single") }),
1316
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
1317
+ ]).optional(),
1318
+ function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
1319
+ prefetchDefaults: zod.z.object({
1320
+ maxStalenessMs: zod.z.number().int().positive(),
1321
+ timeoutMs: zod.z.number().int().positive(),
1322
+ onError: zod.z.enum(["fallback", "fail"])
1323
+ }).optional(),
1324
+ writeback: zod.z.object({
1325
+ columns: zod.z.array(zod.z.string()),
1326
+ defaultMode: zod.z.enum(["sync", "async"]),
1327
+ timeoutMs: zod.z.number().int().positive(),
1328
+ clearManualOnConfirm: zod.z.boolean()
1329
+ }).optional()
1330
+ });
1331
+ var resolvedMetaIrSchema = zod.z.object({
1332
+ /** Logical pk column of the resolved view (single-column). */
1333
+ pk: zod.z.string(),
1334
+ /** Source name of the manual source. */
1335
+ manual: zod.z.string(),
1336
+ sources: zod.z.array(resolvedSourceIrSchema),
1337
+ deletedBy: zod.z.array(zod.z.string()),
1338
+ /**
1339
+ * Materialized: the view reads `table`, which the data plane refreshes from
1340
+ * the `compute` view (claims-gated) whenever it writes a source row.
1341
+ */
1342
+ materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
556
1343
  });
557
1344
  var tableMetaIrSchema = zod.z.object({
558
1345
  table: zod.z.string(),
@@ -589,18 +1376,45 @@ var tableMetaIrSchema = zod.z.object({
589
1376
  insertable: zod.z.array(zod.z.string()),
590
1377
  settable: zod.z.array(zod.z.string()),
591
1378
  appendable: zod.z.array(zod.z.string()),
1379
+ /**
1380
+ * Logical columns stamped with Postgres `now()` on UPDATE (canonical
1381
+ * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
1382
+ */
1383
+ touchOnUpdate: zod.z.array(zod.z.string()).optional(),
1384
+ /**
1385
+ * Per-cell metadata: logical column → the logical names of its cell-part
1386
+ * columns (which are listed in `columns`, but never filterable, orderable or
1387
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
1388
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
1389
+ * the relation tracks no cells.
1390
+ */
1391
+ cells: zod.z.record(zod.z.string(), zod.z.object({
1392
+ at: zod.z.string().optional(),
1393
+ by: zod.z.string().optional(),
1394
+ rev: zod.z.string().optional(),
1395
+ source: zod.z.string().optional(),
1396
+ conflicts: zod.z.string().optional()
1397
+ }).strict()).optional(),
1398
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
1399
+ resolved: resolvedMetaIrSchema.optional(),
592
1400
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
593
1401
  /** Depth/node caps for dynamic filter trees on this table. */
594
1402
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
595
1403
  dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
596
1404
  });
597
1405
  var compiledArtifactSchema = zod.z.object({
598
- irVersion: zod.z.literal(IR_VERSION),
1406
+ irVersion: irVersionSchema,
599
1407
  namespace: zod.z.string(),
600
1408
  schemaHash: zod.z.string(),
601
1409
  opsHash: zod.z.string(),
602
1410
  tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
603
1411
  operations: zod.z.record(zod.z.string(), compiledOperationSchema)
1412
+ }).superRefine((artifact, ctx) => {
1413
+ for (const [name, operation] of Object.entries(artifact.operations)) {
1414
+ if (operation.irVersion > artifact.irVersion) {
1415
+ ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
1416
+ }
1417
+ }
604
1418
  });
605
1419
  var IrVersionError = class extends Error {
606
1420
  constructor(found) {
@@ -611,7 +1425,7 @@ var IrVersionError = class extends Error {
611
1425
  function normalizeIr(raw) {
612
1426
  if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
613
1427
  const version = raw.irVersion;
614
- if (version !== IR_VERSION) throw new IrVersionError(version);
1428
+ if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
615
1429
  return compiledArtifactSchema.parse(raw);
616
1430
  }
617
1431
  function indexByHash(artifact) {
@@ -620,11 +1434,16 @@ function indexByHash(artifact) {
620
1434
  return map;
621
1435
  }
622
1436
 
1437
+ exports.CELLS_FIELD = CELLS_FIELD;
1438
+ exports.CELL_PARTS = CELL_PARTS;
623
1439
  exports.CLAIMS_CONTRACT = CLAIMS_CONTRACT;
624
1440
  exports.CMP_OPS = CMP_OPS;
1441
+ exports.DATE_BUCKETS = DATE_BUCKETS;
625
1442
  exports.INPUT_REF_KINDS = INPUT_REF_KINDS;
626
1443
  exports.IR_VERSION = IR_VERSION;
627
1444
  exports.IrVersionError = IrVersionError;
1445
+ exports.LEGACY_IR_VERSION = LEGACY_IR_VERSION;
1446
+ exports.cellColumnName = cellColumnName;
628
1447
  exports.compiledArtifactSchema = compiledArtifactSchema;
629
1448
  exports.compiledOperationSchema = compiledOperationSchema;
630
1449
  exports.indexByHash = indexByHash;
@@ -632,6 +1451,10 @@ exports.limitIrSchema = limitIrSchema;
632
1451
  exports.normalizeIr = normalizeIr;
633
1452
  exports.orderIrSchema = orderIrSchema;
634
1453
  exports.predIrSchema = predIrSchema;
1454
+ exports.prefetchPlanIrSchema = prefetchPlanIrSchema;
1455
+ exports.requiredArtifactIrVersion = requiredArtifactIrVersion;
1456
+ exports.resolvedMetaIrSchema = resolvedMetaIrSchema;
1457
+ exports.resolvedSourceIrSchema = resolvedSourceIrSchema;
635
1458
  exports.rootIrSchema = rootIrSchema;
636
1459
  exports.selectionIrSchema = selectionIrSchema;
637
1460
  exports.tableMetaIrSchema = tableMetaIrSchema;