@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/sdl.js CHANGED
@@ -1,10 +1,481 @@
1
1
  import { z } from 'zod';
2
2
  import { createHash } from 'crypto';
3
3
 
4
+ // src/canonical/v2.ts
5
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
6
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
7
+
8
+ // src/checks/compile.ts
9
+ function tokenize(source) {
10
+ const tokens = [];
11
+ for (let position = 0; position < source.length; ) {
12
+ const char = source[position];
13
+ if (/\s/.test(char)) {
14
+ position++;
15
+ continue;
16
+ }
17
+ if (char === "(" || char === ")" || char === ",") {
18
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
19
+ position++;
20
+ continue;
21
+ }
22
+ if (char === "'") {
23
+ const start = position++;
24
+ let value = "";
25
+ let closed = false;
26
+ while (position < source.length) {
27
+ if (source[position] !== "'") {
28
+ value += source[position++];
29
+ continue;
30
+ }
31
+ if (source[position + 1] === "'") {
32
+ value += "'";
33
+ position += 2;
34
+ continue;
35
+ }
36
+ position++;
37
+ closed = true;
38
+ break;
39
+ }
40
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
41
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
42
+ tokens.push({ kind: "string", value, position: start });
43
+ continue;
44
+ }
45
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
46
+ if (operator) {
47
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
48
+ position += operator[0].length;
49
+ continue;
50
+ }
51
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
52
+ if (number) {
53
+ tokens.push({ kind: "number", value: number[0], position });
54
+ position += number[0].length;
55
+ continue;
56
+ }
57
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
58
+ if (identifier3) {
59
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
60
+ position += identifier3[0].length;
61
+ continue;
62
+ }
63
+ throw new Error(`unexpected character '${char}' at position ${position}`);
64
+ }
65
+ tokens.push({ kind: "end", value: "", position: source.length });
66
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
67
+ return tokens;
68
+ }
69
+ var Parser = class {
70
+ constructor(tokens, columns) {
71
+ this.tokens = tokens;
72
+ this.columns = /* @__PURE__ */ new Map();
73
+ for (const column of columns) {
74
+ for (const alias of [column.name, column.columnName]) {
75
+ const previous = this.columns.get(alias);
76
+ if (previous && previous.columnName !== column.columnName) {
77
+ throw new Error(`ambiguous check column '${alias}'`);
78
+ }
79
+ this.columns.set(alias, column);
80
+ }
81
+ }
82
+ }
83
+ index = 0;
84
+ depth = 0;
85
+ columns;
86
+ parse() {
87
+ const expression = this.parseOr();
88
+ this.expect("end", "end of expression");
89
+ return expression;
90
+ }
91
+ parseOr() {
92
+ let left = this.parseAnd();
93
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
94
+ return left;
95
+ }
96
+ parseAnd() {
97
+ let left = this.parseNot();
98
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
99
+ return left;
100
+ }
101
+ parseNot() {
102
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
103
+ return this.parsePrimary();
104
+ }
105
+ parsePrimary() {
106
+ if (this.peek().kind === "lparen") {
107
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
108
+ this.advance();
109
+ const expression = this.parseOr();
110
+ this.expect("rparen", "')'");
111
+ this.depth--;
112
+ return `(${expression})`;
113
+ }
114
+ return this.parseComparison();
115
+ }
116
+ parseComparison() {
117
+ const left = this.parseValue();
118
+ if (this.matchKeyword("is")) {
119
+ const negate = this.matchKeyword("not");
120
+ this.expectKeyword("null");
121
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
122
+ }
123
+ if (this.matchKeyword("between")) {
124
+ const lower = this.parseValue();
125
+ this.expectKeyword("and");
126
+ const upper = this.parseValue();
127
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
128
+ }
129
+ if (this.matchKeyword("in")) {
130
+ this.expect("lparen", "'(' after IN");
131
+ const values = [this.parseValue()];
132
+ while (this.peek().kind === "comma") {
133
+ this.advance();
134
+ values.push(this.parseValue());
135
+ }
136
+ this.expect("rparen", "')' after IN values");
137
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
138
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
139
+ }
140
+ const operator = this.expect("operator", "a comparison operator");
141
+ const right = this.parseValue();
142
+ if (operator.value === "~" || operator.value === "!~") {
143
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
144
+ if (!right.literal) throw new Error("regex pattern must be a literal");
145
+ }
146
+ return `${left.sql} ${operator.value} ${right.sql}`;
147
+ }
148
+ parseValue() {
149
+ const token = this.advance();
150
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
151
+ if (token.kind === "number") {
152
+ const number = Number(token.value);
153
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
154
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
155
+ }
156
+ return { sql: token.value, literal: true };
157
+ }
158
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
159
+ const keyword = token.value.toLowerCase();
160
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
161
+ if (keyword === "btrim") {
162
+ this.expect("lparen", "'(' after btrim");
163
+ const column2 = this.parseColumn();
164
+ this.expect("rparen", "')' after btrim column");
165
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
166
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
167
+ }
168
+ const column = this.columns.get(token.value);
169
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
170
+ return { sql: quoteIdentifier(column.columnName), column };
171
+ }
172
+ parseColumn() {
173
+ const token = this.expect("identifier", "a column name");
174
+ const column = this.columns.get(token.value);
175
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
176
+ return column;
177
+ }
178
+ peek() {
179
+ return this.tokens[this.index];
180
+ }
181
+ advance() {
182
+ const token = this.peek();
183
+ if (token.kind !== "end") this.index++;
184
+ return token;
185
+ }
186
+ matchKeyword(keyword) {
187
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
188
+ this.advance();
189
+ return true;
190
+ }
191
+ expectKeyword(keyword) {
192
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
193
+ }
194
+ expect(kind, description) {
195
+ const token = this.peek();
196
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
197
+ return this.advance();
198
+ }
199
+ };
200
+ function compileCheckExpression(source, columns) {
201
+ return new Parser(tokenize(source), columns).parse();
202
+ }
203
+
204
+ // src/constants.ts
205
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
206
+ var MAX_IDENTIFIER_LENGTH = 63;
207
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
208
+ var CELL_PART_SUFFIX = {
209
+ at: "__at",
210
+ by: "__by",
211
+ rev: "__rev",
212
+ source: "__src",
213
+ conflicts: "__lost"
214
+ };
215
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
216
+ var CELLS_FIELD = "cells";
217
+ function cellColumnName(column, part) {
218
+ return `${column}${CELL_PART_SUFFIX[part]}`;
219
+ }
220
+ var MATERIALIZE_ACTOR = "orm:materialize";
221
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
222
+ var policyVariantSchema = z.object({
223
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
224
+ when: z.string().min(1).max(1e3),
225
+ /** Exactly one of: allow-all bypass, or a row predicate. */
226
+ allow: z.literal(true).optional(),
227
+ policy: z.string().min(1).max(2e3).optional()
228
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
229
+ message: "A variant declares exactly one of allow: true or policy"
230
+ });
231
+ var policyExpressionSchema = z.union([
232
+ z.string().min(1).max(2e3),
233
+ z.object({
234
+ default: z.string().min(1).max(2e3),
235
+ variants: z.array(policyVariantSchema).max(10).default([])
236
+ })
237
+ ]);
238
+ var identifier = z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
239
+ var durationMs = z.number().int().positive().max(24 * 60 * 60 * 1e3);
240
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
241
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
242
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
243
+ };
244
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
245
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
246
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
247
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
248
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
249
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
250
+ };
251
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
252
+ /* @__PURE__ */ new Set([
253
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
254
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
255
+ ]);
256
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
257
+ var canonicalFunctionRefSchema = z.object({
258
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
259
+ kind: z.enum(["managed", "native"]),
260
+ id: z.string().min(1).max(200)
261
+ }).strict();
262
+ var canonicalFetchShapeSchema = z.union([
263
+ z.object({ shape: z.literal("single") }).strict(),
264
+ z.object({ shape: z.literal("batch"), maxKeys: z.number().int().min(2).max(500) }).strict()
265
+ ]);
266
+ var canonicalResolvedSourceSchema = z.object({
267
+ name: identifier,
268
+ kind: z.enum(["manual", "etl", "prefetch"]),
269
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
270
+ table: identifier,
271
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
272
+ columns: z.array(z.string().min(1).max(63)).min(1).max(500),
273
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
274
+ updatedAt: z.string().min(1).max(63).optional(),
275
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
276
+ key: z.string().min(1).max(63).optional(),
277
+ fetch: canonicalFetchShapeSchema.optional(),
278
+ function: canonicalFunctionRefSchema.optional(),
279
+ prefetchDefaults: z.object({
280
+ maxStalenessMs: durationMs,
281
+ timeoutMs: durationMs,
282
+ onError: z.enum(["fallback", "fail"])
283
+ }).strict().optional(),
284
+ writeback: z.object({
285
+ columns: z.array(z.string().min(1).max(63)).min(1).max(500),
286
+ defaultMode: z.enum(["sync", "async"]),
287
+ timeoutMs: durationMs,
288
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
289
+ clearManualOnConfirm: z.boolean()
290
+ }).strict().optional()
291
+ }).strict();
292
+ var canonicalResolveRuleSchema = z.object({
293
+ column: z.string().min(1).max(63),
294
+ comparator: z.enum(COMPARATORS),
295
+ /** Source names, in tie-break order. */
296
+ candidates: z.array(identifier).min(1).max(16),
297
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
298
+ orderBy: z.string().min(1).max(2e3).optional()
299
+ }).strict();
300
+ var canonicalResolvedSchema = z.object({
301
+ sources: z.array(canonicalResolvedSourceSchema).min(1).max(16),
302
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
303
+ resolve: z.array(canonicalResolveRuleSchema).max(500),
304
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
305
+ deletedBy: z.array(identifier).max(1).default([]),
306
+ /**
307
+ * Materialized: the view reads an indexed internal table (`table`) that the
308
+ * data plane keeps in step with `compute`, the internal view holding the
309
+ * resolution query. Omitted when the view computes on read.
310
+ */
311
+ materialized: z.object({ table: identifier, compute: identifier }).strict().optional(),
312
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
313
+ policies: z.object({
314
+ write: policyExpressionSchema.optional(),
315
+ insert: policyExpressionSchema.optional(),
316
+ update: policyExpressionSchema.optional()
317
+ }).strict().default({})
318
+ }).strict();
319
+ function validateResolvedView({ view, tables, views = [] }) {
320
+ const resolved = view.resolved;
321
+ if (!resolved) return [];
322
+ const errors = [];
323
+ const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
324
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
325
+ if (!pkColumn) {
326
+ fail("requires exactly one primary-key column");
327
+ return errors;
328
+ }
329
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
330
+ const scope = {
331
+ view,
332
+ resolved,
333
+ pkColumn,
334
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
335
+ sourceByName: /* @__PURE__ */ new Map(),
336
+ fail
337
+ };
338
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
339
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
340
+ for (const source of resolved.sources) {
341
+ if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
342
+ scope.sourceByName.set(source.name, source);
343
+ const table = tableByName.get(source.table);
344
+ if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
345
+ else validateSource({ scope, source, table });
346
+ }
347
+ validateRules(scope);
348
+ validateViewCells(scope);
349
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
350
+ for (const name of resolved.deletedBy) {
351
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
352
+ }
353
+ return errors;
354
+ }
355
+ function validateSource({ scope, source, table }) {
356
+ const { fail } = scope;
357
+ if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
358
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
359
+ fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
360
+ }
361
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
362
+ if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
363
+ if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
364
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
365
+ validateSourceColumns({ scope, source, byName });
366
+ if (source.kind === "etl") {
367
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
368
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
369
+ fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
370
+ }
371
+ }
372
+ validatePrefetchFields({ scope, source });
373
+ }
374
+ function validatePrefetchFields({ scope, source }) {
375
+ const { fail, pkColumn } = scope;
376
+ if (source.kind !== "prefetch") {
377
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
378
+ fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
379
+ }
380
+ return;
381
+ }
382
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
383
+ fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
384
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
385
+ fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
386
+ }
387
+ for (const column of source.writeback?.columns ?? []) {
388
+ if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
389
+ }
390
+ }
391
+ function validateSourceColumns({ scope, source, byName }) {
392
+ const { fail, pkColumn, dataColumns } = scope;
393
+ for (const column of [pkColumn.name, ...source.columns]) {
394
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
395
+ const theirs = byName.get(column);
396
+ if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
397
+ else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
398
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
399
+ }
400
+ }
401
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
402
+ const ruled = /* @__PURE__ */ new Set();
403
+ for (const rule of resolved.resolve) {
404
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
405
+ fail(`resolve rule targets unknown column '${rule.column}'`);
406
+ continue;
407
+ }
408
+ if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
409
+ ruled.add(rule.column);
410
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
411
+ fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
412
+ }
413
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
414
+ for (const candidate of rule.candidates) {
415
+ const source = sourceByName.get(candidate);
416
+ if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
417
+ else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
418
+ }
419
+ }
420
+ for (const column of dataColumns.keys()) {
421
+ if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
422
+ }
423
+ }
424
+ var VIEW_CELL_TYPES = {
425
+ source: "text",
426
+ at: "timestamptz",
427
+ by: "text",
428
+ conflicts: "jsonb"
429
+ };
430
+ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
431
+ const expected = /* @__PURE__ */ new Map();
432
+ for (const column of dataColumns.values()) {
433
+ if (column.name === pkColumn.name) continue;
434
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
435
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
436
+ }
437
+ }
438
+ for (const column of view.columns) {
439
+ if (!column.cell) continue;
440
+ const want = expected.get(column.name);
441
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
442
+ fail(`unexpected cell column '${column.name}'`);
443
+ continue;
444
+ }
445
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
446
+ expected.delete(column.name);
447
+ }
448
+ for (const name of expected.keys()) fail(`missing cell column '${name}'`);
449
+ }
450
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
451
+ function validateMaterialized({ scope, tableByName, views }) {
452
+ const { view, resolved, fail } = scope;
453
+ const { table: tableName, compute: computeName } = resolved.materialized;
454
+ const table = tableByName.get(tableName);
455
+ if (!table || table.internal !== true || table.materializes !== view.view) {
456
+ fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
457
+ } else {
458
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
459
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
460
+ }
461
+ const compute = views.find((candidate) => candidate.view === computeName);
462
+ if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
463
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
464
+ for (const source of resolved.sources) {
465
+ if (source.kind !== "etl") continue;
466
+ const read = tableByName.get(source.table)?.policies?.read;
467
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
468
+ if (!unconditional) {
469
+ fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
470
+ }
471
+ }
472
+ }
473
+
4
474
  // src/canonical/v2.ts
5
475
  var CANONICAL_FORMAT = 2;
6
476
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
7
- var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
477
+ var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
478
+ var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
8
479
  var COLUMN_TYPES = [
9
480
  "uuid",
10
481
  "text",
@@ -19,59 +490,71 @@ var COLUMN_TYPES = [
19
490
  ];
20
491
  var columnTypeSchema = z.enum(COLUMN_TYPES);
21
492
  var canonicalEnumSchema = z.object({
22
- name: identifier,
493
+ name: identifier2,
23
494
  values: z.array(z.string().min(1).max(63)).min(1).max(100)
24
495
  });
25
496
  var canonicalColumnSchema = z.object({
26
497
  /** Property name in TS (camelCase); columnName is the Postgres name. */
27
498
  name: z.string().min(1).max(63),
28
- columnName: identifier,
29
- type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
499
+ columnName: columnIdentifier,
500
+ type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
30
501
  nullable: z.boolean().default(true),
31
502
  unique: z.boolean().default(false),
32
503
  /** Literal default, or one of the allowlisted SQL functions. */
33
504
  default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
34
505
  defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
506
+ /**
507
+ * Stamp this column from the database clock on UPDATE (`now()`). The
508
+ * column stays insertable (defaults fill gaps) but is omitted from
509
+ * SetInput / settable so callers cannot backdate it.
510
+ */
511
+ onUpdateFn: z.enum(["now()"]).optional(),
35
512
  description: z.string().max(500).optional(),
36
513
  /** Migration hints — same semantics as v1. */
37
- renamedFrom: identifier.optional(),
514
+ renamedFrom: columnIdentifier.optional(),
38
515
  cast: z.string().max(200).optional(),
39
- deprecated: z.boolean().default(false)
516
+ deprecated: z.boolean().default(false),
517
+ /**
518
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
519
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
520
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
521
+ * ordinary columns so their hashes are unchanged.
522
+ */
523
+ cell: z.object({ of: z.string().min(1).max(63), part: z.enum(CELL_PARTS) }).strict().optional()
524
+ }).superRefine((column, ctx) => {
525
+ if (column.onUpdateFn === void 0) return;
526
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
527
+ ctx.addIssue({
528
+ code: "custom",
529
+ path: ["onUpdateFn"],
530
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
531
+ });
532
+ }
40
533
  });
41
534
  var canonicalLinkSchema = z.object({
42
535
  name: z.string().min(1).max(63),
43
536
  kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
44
537
  /** Target table name (namespace-local). Absent for toPlatformUser. */
45
- target: identifier.optional(),
538
+ target: identifier2.optional(),
46
539
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
47
- on: identifier,
540
+ on: columnIdentifier,
48
541
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
49
- references: identifier.optional(),
542
+ references: columnIdentifier.optional(),
50
543
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
51
544
  enforce: z.boolean().default(false),
545
+ onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
546
+ onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
52
547
  description: z.string().max(500).optional()
53
548
  });
54
549
  var canonicalIndexSchema = z.object({
55
- columns: z.array(identifier).min(1),
550
+ columns: z.array(columnIdentifier).min(1),
56
551
  unique: z.boolean().default(false),
57
- name: identifier.optional()
552
+ name: identifier2.optional()
58
553
  });
59
- var policyVariantSchema = z.object({
60
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
61
- when: z.string().min(1).max(1e3),
62
- /** Exactly one of: allow-all bypass, or a row predicate. */
63
- allow: z.literal(true).optional(),
64
- policy: z.string().min(1).max(2e3).optional()
65
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
66
- message: "A variant declares exactly one of allow: true or policy"
67
- });
68
- var policyExpressionSchema = z.union([
69
- z.string().min(1).max(2e3),
70
- z.object({
71
- default: z.string().min(1).max(2e3),
72
- variants: z.array(policyVariantSchema).max(10).default([])
73
- })
74
- ]);
554
+ var canonicalCheckSchema = z.object({
555
+ name: identifier2,
556
+ expression: z.string().min(1).max(2e3)
557
+ }).strict();
75
558
  var canonicalPoliciesSchema = z.object({
76
559
  read: policyExpressionSchema.optional(),
77
560
  write: policyExpressionSchema.optional(),
@@ -102,7 +585,7 @@ function isSingleViewQuerySql(value) {
102
585
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
103
586
  }
104
587
  var canonicalActionSchema = z.object({
105
- name: identifier,
588
+ name: identifier2,
106
589
  requires: z.string().max(1e3).optional(),
107
590
  params: z.record(z.string(), z.object({
108
591
  type: columnTypeSchema,
@@ -114,9 +597,9 @@ var canonicalActionSchema = z.object({
114
597
  });
115
598
  var canonicalCowMutationSchema = z.object({
116
599
  kind: z.enum(["override", "revert"]),
117
- name: identifier,
600
+ name: identifier2,
118
601
  /** Synced table (same namespace) the overlay copies from / falls back to. */
119
- synced: identifier,
602
+ synced: identifier2,
120
603
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
121
604
  pk: z.string().min(1).max(63),
122
605
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -143,17 +626,28 @@ var canonicalCowMutationSchema = z.object({
143
626
  }
144
627
  }
145
628
  });
629
+ var canonicalForeignKeySchema = z.object({
630
+ name: identifier2,
631
+ columns: z.array(columnIdentifier).min(1).max(8),
632
+ target: identifier2,
633
+ references: z.array(columnIdentifier).min(1).max(8),
634
+ deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional(),
635
+ onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
636
+ onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
637
+ }).strict();
146
638
  var canonicalTableSchema = z.object({
147
- table: identifier,
639
+ table: identifier2,
148
640
  description: z.string().max(2e3).optional(),
149
641
  /** Table-level rename hint (new in v2). */
150
- renamedFrom: identifier.optional(),
642
+ renamedFrom: identifier2.optional(),
151
643
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
152
644
  source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
153
645
  columns: z.array(canonicalColumnSchema).default([]),
154
- primaryKey: z.array(identifier).min(1).max(8).optional(),
646
+ primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
155
647
  links: z.array(canonicalLinkSchema).default([]),
156
648
  indexes: z.array(canonicalIndexSchema).default([]),
649
+ /** Omitted by old definitions, preserving their existing content hashes. */
650
+ checks: z.array(canonicalCheckSchema).max(100).optional(),
157
651
  policies: canonicalPoliciesSchema.default({}),
158
652
  /**
159
653
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -162,26 +656,64 @@ var canonicalTableSchema = z.object({
162
656
  */
163
657
  actions: z.array(canonicalActionSchema).default([]),
164
658
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
165
- cowMutations: z.array(canonicalCowMutationSchema).optional()
659
+ cowMutations: z.array(canonicalCowMutationSchema).optional(),
660
+ /** Omitted by old definitions, preserving their existing content hashes. */
661
+ foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional(),
662
+ /**
663
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
664
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
665
+ * on a resolved table's internal sources), listed explicitly in `columns`
666
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
667
+ * Omitted when off so existing definitions hash unchanged.
668
+ */
669
+ cellMetadata: z.literal(true).optional(),
670
+ /**
671
+ * System-owned backing table of a resolved table (its manual or prefetch
672
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
673
+ * only the data plane writes them. Omitted when false.
674
+ */
675
+ internal: z.literal(true).optional(),
676
+ /**
677
+ * The materialized copy of resolved table `materializes` (internal): the
678
+ * same columns as that view, including its cell parts, maintained by the
679
+ * data plane. Omitted otherwise.
680
+ */
681
+ materializes: identifier2.optional()
166
682
  });
683
+ var MAX_VIEW_QUERY = 1e5;
684
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
167
685
  var canonicalViewSchema = z.object({
168
- view: identifier,
686
+ view: identifier2,
169
687
  description: z.string().max(2e3).optional(),
170
688
  columns: z.array(canonicalColumnSchema).min(1),
171
689
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
172
- primaryKey: z.array(identifier).min(1).max(8).optional(),
690
+ primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
173
691
  links: z.array(canonicalLinkSchema).default([]),
174
692
  policies: z.object({ read: policyExpressionSchema }).strict(),
175
- query: z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
693
+ query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
176
694
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
177
695
  }),
178
696
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
179
- dependencies: z.array(identifier).max(500)
697
+ dependencies: z.array(identifier2).max(500),
698
+ /**
699
+ * Present when this view is a resolved table (defineResolvedTable): the
700
+ * multi-source metadata behind its generated query. Omitted otherwise so
701
+ * existing views hash unchanged.
702
+ */
703
+ resolved: canonicalResolvedSchema.optional(),
704
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
705
+ internal: z.literal(true).optional()
180
706
  }).superRefine((view, ctx) => {
707
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
708
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
709
+ }
181
710
  for (const [index, column] of view.columns.entries()) {
182
711
  if (column.default !== void 0 || column.defaultFn !== void 0) {
183
712
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
184
713
  }
714
+ if (column.onUpdateFn !== void 0) {
715
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
716
+ }
185
717
  if (column.unique) {
186
718
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
187
719
  }
@@ -189,15 +721,82 @@ var canonicalViewSchema = z.object({
189
721
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
190
722
  }
191
723
  }
724
+ if (view.resolved === void 0 && view.internal !== true) {
725
+ for (const [index, column] of view.columns.entries()) {
726
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
727
+ }
728
+ }
192
729
  for (const [index, link] of view.links.entries()) {
193
730
  if (link.enforce) {
194
731
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
195
732
  }
733
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
734
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
735
+ }
196
736
  }
197
737
  });
738
+ var CELL_PART_TYPES = {
739
+ at: "timestamptz",
740
+ by: "text",
741
+ rev: "uuid",
742
+ source: "text",
743
+ conflicts: "jsonb"
744
+ };
745
+ function trackedCellColumns(table) {
746
+ const pk = new Set(table.primaryKey ?? []);
747
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
748
+ }
749
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
750
+ function validateCellMetadata(table, index, ctx) {
751
+ const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
752
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
753
+ if (table.materializes !== void 0) return;
754
+ if (table.cellMetadata !== true) {
755
+ for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
756
+ return;
757
+ }
758
+ if (table.source !== void 0) {
759
+ fail(`synced table '${table.table}' cannot track cell metadata`);
760
+ return;
761
+ }
762
+ const tracked = trackedCellColumns(table);
763
+ const byName = new Map(tracked.map((column) => [column.name, column]));
764
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
765
+ if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
766
+ const seen = /* @__PURE__ */ new Set();
767
+ for (const { column, position, cell } of cells) {
768
+ const path = ["tables", index, "columns", position];
769
+ const owner = byName.get(cell.of);
770
+ if (!owner) {
771
+ fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
772
+ continue;
773
+ }
774
+ 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);
775
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
776
+ fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
777
+ }
778
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
779
+ fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
780
+ }
781
+ const key = `${cell.of}:${cell.part}`;
782
+ if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
783
+ seen.add(key);
784
+ }
785
+ for (const column of tracked) {
786
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
787
+ fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
788
+ }
789
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
790
+ fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
791
+ }
792
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
793
+ if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
794
+ }
795
+ }
796
+ }
198
797
  var canonicalDefinitionSchema = z.object({
199
798
  definition_format: z.literal(CANONICAL_FORMAT),
200
- namespace: identifier.max(41),
799
+ namespace: identifier2.max(41),
201
800
  description: z.string().max(2e3).optional(),
202
801
  enums: z.array(canonicalEnumSchema).default([]),
203
802
  tables: z.array(canonicalTableSchema).min(0).max(500),
@@ -215,6 +814,74 @@ var canonicalDefinitionSchema = z.object({
215
814
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
216
815
  }
217
816
  seen.add(table.table);
817
+ validateCellMetadata(table, index, ctx);
818
+ if (table.source !== void 0) {
819
+ for (const [columnIndex, column] of table.columns.entries()) {
820
+ if (column.onUpdateFn !== void 0) {
821
+ ctx.addIssue({
822
+ code: "custom",
823
+ path: ["tables", index, "columns", columnIndex],
824
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
825
+ });
826
+ }
827
+ }
828
+ }
829
+ }
830
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
831
+ for (const [tableIndex, table] of definition.tables.entries()) {
832
+ const names = /* @__PURE__ */ new Set([
833
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
834
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
835
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
836
+ ]);
837
+ for (const [index, link] of table.links.entries()) {
838
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
839
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
840
+ }
841
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
842
+ const source = table.columns.find((column) => column.columnName === link.on);
843
+ 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" });
844
+ }
845
+ }
846
+ for (const [index, check] of (table.checks ?? []).entries()) {
847
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
848
+ if (table.source) fail("synced tables cannot declare checks");
849
+ if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
850
+ names.add(check.name);
851
+ try {
852
+ compileCheckExpression(check.expression, table.columns);
853
+ } catch (error) {
854
+ fail(error instanceof Error ? error.message : String(error));
855
+ }
856
+ }
857
+ for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
858
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
859
+ if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
860
+ names.add(fk.name);
861
+ const target = fkTables.get(fk.target);
862
+ if (table.source || !target || target.source) {
863
+ fail("foreign keys must connect owned tables in the same namespace");
864
+ continue;
865
+ }
866
+ if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
867
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
868
+ for (const [position, name] of fk.columns.entries()) {
869
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
870
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
871
+ if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
872
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
873
+ }
874
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
875
+ 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");
876
+ }
877
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
878
+ 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");
879
+ }
880
+ }
881
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
882
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
883
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
884
+ }
218
885
  }
219
886
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
220
887
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -339,8 +1006,12 @@ function canonicalRelations(definition) {
339
1006
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
340
1007
  links: view.links,
341
1008
  indexes: [],
342
- policies: { read: view.policies.read },
343
- actions: []
1009
+ // A resolved table's write policies (evaluated against the resolved row)
1010
+ // ride alongside the view's read policy.
1011
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
1012
+ actions: [],
1013
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
1014
+ ...view.internal ? { internal: true } : {}
344
1015
  }));
345
1016
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
346
1017
  }
@@ -350,12 +1021,14 @@ function normalizeCanonical(definition) {
350
1021
  ...definition,
351
1022
  enums: sortByName(definition.enums),
352
1023
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
353
- const { cowMutations, ...rest } = t;
1024
+ const { cowMutations, foreignKeys, checks, ...rest } = t;
354
1025
  return {
355
1026
  ...rest,
356
1027
  links: sortByName(t.links),
357
1028
  indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
358
1029
  actions: sortByName(t.actions),
1030
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1031
+ ...checks?.length ? { checks: sortByName(checks) } : {},
359
1032
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
360
1033
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
361
1034
  };
@@ -444,6 +1117,8 @@ var ORDER_BY_ENUM_VALUES = [
444
1117
  "DESC_NULLS_LAST"
445
1118
  ];
446
1119
  var PLATFORM_USER_TYPENAME = "PlatformUser";
1120
+ var DATE_BUCKET_ENUM = "DateBucket";
1121
+ var BUCKETABLE_COLUMN_TYPES = ["timestamptz", "date"];
447
1122
  function tableNames(table) {
448
1123
  const root = camelCase(table);
449
1124
  const rootPascal = pascalCase(table);
@@ -459,6 +1134,7 @@ function tableNames(table) {
459
1134
  updateByPkRoot: `update${rootPascal}ByPk`,
460
1135
  deleteRoot: `delete${rootPascal}`,
461
1136
  deleteByPkRoot: `delete${rootPascal}ByPk`,
1137
+ revertByPkRoot: `revert${rootPascal}ByPk`,
462
1138
  boolExp: `${type}BoolExp`,
463
1139
  orderBy: `${type}OrderBy`,
464
1140
  selectColumn: `${type}SelectColumn`,
@@ -476,9 +1152,426 @@ function tableNames(table) {
476
1152
  sumFields: `${type}SumFields`,
477
1153
  avgFields: `${type}AvgFields`,
478
1154
  aggregateBoolExp: `${type}AggregateBoolExp`,
479
- aggregateBoolExpCount: `${type}AggregateBoolExpCount`
1155
+ aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
1156
+ aggregateGroupsRoot: `${root}AggregateGroups`,
1157
+ groupByColumnEnum: `${type}GroupByColumn`,
1158
+ groupByInput: `${type}GroupBy`,
1159
+ groupKey: `${type}GroupKey`,
1160
+ aggregateGroup: `${type}AggregateGroup`
480
1161
  };
481
1162
  }
1163
+ var CELL_TYPENAMES = {
1164
+ /** A plain table's cell: when and by whom it was last written. */
1165
+ stamp: "CellStamp",
1166
+ /** A resolved table's cell: which source won, when, by whom, and newer losers. */
1167
+ resolved: "ResolvedCell",
1168
+ /** A newer candidate that lost to the column's rule. */
1169
+ conflict: "CellConflict"
1170
+ };
1171
+ function cellsTypename(typename) {
1172
+ return `${typename}Cells`;
1173
+ }
1174
+ var IR_VERSION = 2;
1175
+ var LEGACY_IR_VERSION = 1;
1176
+ var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(IR_VERSION)]);
1177
+ z.object({ irVersion: irVersionSchema });
1178
+ var scalarLiteral = z.union([z.string(), z.number(), z.boolean(), z.null()]);
1179
+ var valueIrSchema = z.discriminatedUnion("kind", [
1180
+ z.object({ kind: z.literal("literal"), value: z.union([scalarLiteral, z.array(scalarLiteral)]) }),
1181
+ z.object({ kind: z.literal("variable"), name: z.string() }),
1182
+ z.object({ kind: z.literal("claim"), key: z.enum(["sub", "org"]) })
1183
+ ]);
1184
+ var CMP_OPS = [
1185
+ "eq",
1186
+ "neq",
1187
+ "gt",
1188
+ "gte",
1189
+ "lt",
1190
+ "lte",
1191
+ "in",
1192
+ "nin",
1193
+ "like",
1194
+ "ilike",
1195
+ "isNull",
1196
+ "contains",
1197
+ "hasKey"
1198
+ ];
1199
+ var predIrSchema = z.lazy(
1200
+ () => z.discriminatedUnion("kind", [
1201
+ z.object({ kind: z.literal("and"), operands: z.array(predIrSchema).min(1) }),
1202
+ z.object({ kind: z.literal("or"), operands: z.array(predIrSchema).min(1) }),
1203
+ z.object({ kind: z.literal("not"), operand: predIrSchema }),
1204
+ z.object({ kind: z.literal("cmp"), op: z.enum(CMP_OPS), column: z.string(), value: valueIrSchema }),
1205
+ z.object({ kind: z.literal("exists"), link: z.string(), where: predIrSchema }),
1206
+ z.object({
1207
+ kind: z.literal("aggPredicate"),
1208
+ link: z.string(),
1209
+ fn: z.literal("count"),
1210
+ op: z.enum(["eq", "neq", "gt", "gte", "lt", "lte"]),
1211
+ value: z.number(),
1212
+ where: predIrSchema.optional()
1213
+ }),
1214
+ z.object({ kind: z.literal("capability"), key: z.string() }),
1215
+ z.object({ kind: z.literal("role"), key: z.string() }),
1216
+ z.object({ kind: z.literal("dynamicFilter"), slot: z.string() }),
1217
+ z.object({ kind: z.literal("const"), value: z.boolean() })
1218
+ ])
1219
+ );
1220
+ var orderIrSchema = z.discriminatedUnion("kind", [
1221
+ z.object({
1222
+ kind: z.literal("column"),
1223
+ column: z.string(),
1224
+ /** Order through a to-one link: `link` is set and `column` is on the target. */
1225
+ link: z.string().optional(),
1226
+ direction: z.enum(["asc", "desc"]),
1227
+ nulls: z.enum(["first", "last"]).optional()
1228
+ }),
1229
+ /** §4f: `$orderBy: [FooOrderBy!]` variable — validated + lowered at runtime. */
1230
+ z.object({ kind: z.literal("dynamic"), slot: z.string() })
1231
+ ]);
1232
+ var limitIrSchema = z.object({
1233
+ /** Fixed value or a variable, clamped to [1, max]; absent → default. */
1234
+ variable: z.string().optional(),
1235
+ value: z.number().int().positive().optional(),
1236
+ default: z.number().int().positive(),
1237
+ max: z.number().int().positive()
1238
+ });
1239
+ var selectionIrSchema = z.lazy(
1240
+ () => z.object({
1241
+ typename: z.string(),
1242
+ columns: z.array(z.object({ alias: z.string(), column: z.string() })),
1243
+ cells: z.object({
1244
+ alias: z.string(),
1245
+ typename: z.string(),
1246
+ fields: z.array(z.object({
1247
+ alias: z.string(),
1248
+ column: z.string(),
1249
+ typename: z.string(),
1250
+ parts: z.array(z.object({
1251
+ alias: z.string(),
1252
+ part: z.enum(["source", "at", "by", "conflicts"]),
1253
+ fields: z.array(z.object({ alias: z.string(), field: z.enum(["source", "at", "value"]) })).optional(),
1254
+ typename: z.string().optional()
1255
+ }))
1256
+ }))
1257
+ }).optional(),
1258
+ relations: z.array(z.object({
1259
+ alias: z.string(),
1260
+ link: z.string(),
1261
+ selection: selectionIrSchema,
1262
+ where: predIrSchema.optional(),
1263
+ orderBy: z.array(orderIrSchema).optional(),
1264
+ limit: limitIrSchema.optional(),
1265
+ offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional()
1266
+ })),
1267
+ aggregates: z.array(z.object({
1268
+ alias: z.string(),
1269
+ link: z.string().optional(),
1270
+ where: predIrSchema.optional(),
1271
+ count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
1272
+ sum: z.array(z.string()).optional(),
1273
+ avg: z.array(z.string()).optional(),
1274
+ nodes: selectionIrSchema.optional()
1275
+ }))
1276
+ })
1277
+ );
1278
+ var includeIfSchema = z.object({ variable: z.string(), negated: z.boolean() });
1279
+ var prefetchPlanIrSchema = z.object({
1280
+ source: z.string(),
1281
+ maxStalenessMs: z.number().int().positive().optional(),
1282
+ key: z.union([z.object({ one: valueIrSchema }), z.object({ many: valueIrSchema })])
1283
+ });
1284
+ var selectRootSchema = z.object({
1285
+ kind: z.literal("select"),
1286
+ alias: z.string(),
1287
+ table: z.string(),
1288
+ selection: selectionIrSchema,
1289
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
1290
+ where: predIrSchema,
1291
+ orderBy: z.array(orderIrSchema).default([]),
1292
+ distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
1293
+ limit: limitIrSchema,
1294
+ offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
1295
+ includeIf: includeIfSchema.optional(),
1296
+ prefetch: z.array(prefetchPlanIrSchema).optional()
1297
+ });
1298
+ var byPkRootSchema = z.object({
1299
+ kind: z.literal("byPk"),
1300
+ alias: z.string(),
1301
+ table: z.string(),
1302
+ selection: selectionIrSchema,
1303
+ pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
1304
+ /** Policy predicate — ANDed with the PK match. */
1305
+ where: predIrSchema,
1306
+ includeIf: includeIfSchema.optional(),
1307
+ prefetch: z.array(prefetchPlanIrSchema).optional()
1308
+ });
1309
+ var aggregateRootSchema = z.object({
1310
+ kind: z.literal("aggregate"),
1311
+ alias: z.string(),
1312
+ table: z.string(),
1313
+ where: predIrSchema,
1314
+ count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
1315
+ sum: z.array(z.string()).optional(),
1316
+ avg: z.array(z.string()).optional(),
1317
+ nodes: selectionIrSchema.optional(),
1318
+ includeIf: includeIfSchema.optional()
1319
+ });
1320
+ var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
1321
+ var groupByKeyIrSchema = z.object({
1322
+ column: z.string(),
1323
+ bucket: z.enum(DATE_BUCKETS).optional()
1324
+ });
1325
+ var aggregateGroupsRootSchema = z.object({
1326
+ kind: z.literal("aggregateGroups"),
1327
+ alias: z.string(),
1328
+ table: z.string(),
1329
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
1330
+ where: predIrSchema,
1331
+ groupBy: z.array(groupByKeyIrSchema).min(1),
1332
+ /**
1333
+ * The `key { … }` projection: which group columns appear in each entry's key
1334
+ * object, in selection order. A subset of `groupBy` (the compiler rejects a
1335
+ * key field that is not a group key — it would be indeterminate within the
1336
+ * group). Empty when the document selects only `aggregate`.
1337
+ */
1338
+ key: z.array(z.object({ alias: z.string(), column: z.string() })).default([]),
1339
+ count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
1340
+ sum: z.array(z.string()).optional(),
1341
+ avg: z.array(z.string()).optional(),
1342
+ /** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
1343
+ limit: limitIrSchema,
1344
+ offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
1345
+ includeIf: includeIfSchema.optional()
1346
+ });
1347
+ var writeValuesSchema = z.union([
1348
+ z.object({ slot: z.string() }),
1349
+ z.object({ entries: z.array(z.object({ column: z.string(), value: valueIrSchema })) })
1350
+ ]);
1351
+ var onConflictIrSchema = z.union([
1352
+ z.object({ slot: z.string() }),
1353
+ z.object({ constraint: z.string(), updateColumns: z.array(z.string()) })
1354
+ ]);
1355
+ var mutationRootSchema = z.object({
1356
+ kind: z.literal("mutation"),
1357
+ /**
1358
+ * CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
1359
+ * synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
1360
+ * applies `set` — both statements generated by the executor from this
1361
+ * metadata, never authored SQL. `cowRevert` deletes the override so the
1362
+ * coalesce view falls back to synced. Both are gated on a matching synced
1363
+ * row and carry the write policy in `where` like update/delete.
1364
+ */
1365
+ op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
1366
+ /**
1367
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
1368
+ * executor gates on the resolved row (write policy in `where`), writes only
1369
+ * the touched cells into the manual source, then re-checks `insertCheck`
1370
+ * against the resolved row. `revert` clears `revertColumns` from the manual
1371
+ * source; `delete` tombstones the row there.
1372
+ */
1373
+ resolvedAction: z.enum(["insert", "update", "delete", "revert"]).optional(),
1374
+ revertColumns: z.array(z.string()).optional(),
1375
+ /** `@writeback(source:, mode:)` directives on this root. */
1376
+ writeback: z.array(z.object({ source: z.string(), mode: z.enum(["sync", "async"]) })).optional(),
1377
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
1378
+ localOnly: z.array(z.string()).optional(),
1379
+ alias: z.string(),
1380
+ table: z.string(),
1381
+ /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
1382
+ cowSynced: z.string().optional(),
1383
+ /** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
1384
+ cowCopyColumns: z.array(z.string()).optional(),
1385
+ /** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
1386
+ cowCopyCheck: predIrSchema.optional(),
1387
+ /** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
1388
+ objects: z.union([
1389
+ z.object({ slot: z.string() }),
1390
+ z.object({ rows: z.array(z.array(z.object({ column: z.string(), value: valueIrSchema }))) })
1391
+ ]).optional(),
1392
+ /** Server-pinned columns (e.g. author_sub ← claim('sub')) merged into every inserted row. */
1393
+ pinnedColumns: z.array(z.object({ column: z.string(), value: valueIrSchema })).default([]),
1394
+ onConflict: onConflictIrSchema.optional(),
1395
+ set: writeValuesSchema.optional(),
1396
+ append: writeValuesSchema.optional(),
1397
+ inc: writeValuesSchema.optional(),
1398
+ /** update/delete row filter: AND(writePolicy, documentWhere/pk, dynamicFilter?). */
1399
+ where: predIrSchema.optional(),
1400
+ pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).optional(),
1401
+ /** WITH CHECK analogue evaluated against NEW row values (insert/update). */
1402
+ insertCheck: predIrSchema.optional(),
1403
+ returning: selectionIrSchema.nullable(),
1404
+ affectedRows: z.boolean().default(false),
1405
+ /** v2: abort the transaction when the actual write count differs, regardless of selection. */
1406
+ assertAffectedRows: z.number().int().min(0).max(2147483647).optional(),
1407
+ includeIf: includeIfSchema.optional()
1408
+ });
1409
+ var rootIrSchema = z.discriminatedUnion("kind", [
1410
+ selectRootSchema,
1411
+ byPkRootSchema,
1412
+ aggregateRootSchema,
1413
+ aggregateGroupsRootSchema,
1414
+ mutationRootSchema
1415
+ ]);
1416
+ var INPUT_REF_KINDS = [
1417
+ "boolExp",
1418
+ "orderBy",
1419
+ "selectColumn",
1420
+ "insertInput",
1421
+ "setInput",
1422
+ "appendInput",
1423
+ "onConflict"
1424
+ ];
1425
+ var variableDefaultIrSchema = z.object({
1426
+ value: z.union([scalarLiteral, z.array(scalarLiteral)])
1427
+ });
1428
+ var variableIrSchema = z.object({
1429
+ name: z.string(),
1430
+ type: z.union([
1431
+ z.object({ kind: z.literal("scalar"), scalar: z.enum(COLUMN_TYPES) }),
1432
+ /** `values` is the declared membership whitelist — the executor validates against it. */
1433
+ z.object({ kind: z.literal("enum"), name: z.string(), values: z.array(z.string()).min(1) }),
1434
+ /** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
1435
+ z.object({ kind: z.literal("inputRef"), input: z.enum(INPUT_REF_KINDS), table: z.string() })
1436
+ ]),
1437
+ nullable: z.boolean(),
1438
+ list: z.boolean(),
1439
+ /** Absent = no default declared; see {@link variableDefaultIrSchema}. */
1440
+ defaultValue: variableDefaultIrSchema.optional()
1441
+ });
1442
+ var compiledOperationSchema = z.object({
1443
+ irVersion: irVersionSchema,
1444
+ name: z.string(),
1445
+ opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
1446
+ kind: z.enum(["query", "mutation"]),
1447
+ requires: z.object({
1448
+ capability: z.string().optional(),
1449
+ role: z.string().optional()
1450
+ }).nullable(),
1451
+ variables: z.array(variableIrSchema),
1452
+ /** Every table the op touches — data-token scoping + audit. */
1453
+ tableSet: z.array(z.string()).min(1),
1454
+ roots: z.array(rootIrSchema).min(1)
1455
+ }).superRefine((operation, ctx) => {
1456
+ for (const [index, root] of operation.roots.entries()) {
1457
+ if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
1458
+ if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
1459
+ ctx.addIssue({ code: "custom", path: ["roots", index], message: "affected-row assertions require unconditional mutation roots and IR v2" });
1460
+ }
1461
+ }
1462
+ });
1463
+ var resolvedSourceIrSchema = z.object({
1464
+ name: z.string(),
1465
+ kind: z.enum(["manual", "etl", "prefetch"]),
1466
+ /** Logical table key of the backing relation. */
1467
+ table: z.string(),
1468
+ columns: z.array(z.string()),
1469
+ key: z.string().optional(),
1470
+ fetch: z.union([
1471
+ z.object({ shape: z.literal("single") }),
1472
+ z.object({ shape: z.literal("batch"), maxKeys: z.number().int().positive() })
1473
+ ]).optional(),
1474
+ function: z.object({ kind: z.enum(["managed", "native"]), id: z.string() }).optional(),
1475
+ prefetchDefaults: z.object({
1476
+ maxStalenessMs: z.number().int().positive(),
1477
+ timeoutMs: z.number().int().positive(),
1478
+ onError: z.enum(["fallback", "fail"])
1479
+ }).optional(),
1480
+ writeback: z.object({
1481
+ columns: z.array(z.string()),
1482
+ defaultMode: z.enum(["sync", "async"]),
1483
+ timeoutMs: z.number().int().positive(),
1484
+ clearManualOnConfirm: z.boolean()
1485
+ }).optional()
1486
+ });
1487
+ var resolvedMetaIrSchema = z.object({
1488
+ /** Logical pk column of the resolved view (single-column). */
1489
+ pk: z.string(),
1490
+ /** Source name of the manual source. */
1491
+ manual: z.string(),
1492
+ sources: z.array(resolvedSourceIrSchema),
1493
+ deletedBy: z.array(z.string()),
1494
+ /**
1495
+ * Materialized: the view reads `table`, which the data plane refreshes from
1496
+ * the `compute` view (claims-gated) whenever it writes a source row.
1497
+ */
1498
+ materialized: z.object({ table: z.string(), compute: z.string() }).optional()
1499
+ });
1500
+ var tableMetaIrSchema = z.object({
1501
+ table: z.string(),
1502
+ schema: z.string(),
1503
+ typename: z.string(),
1504
+ /**
1505
+ * True when statements over this relation evaluate a claims-gated synced
1506
+ * view: synced twins (`source: gold.*`) materialize as views whose baked
1507
+ * policy reads `current_setting('request.jwt.claims')`, and namespace views
1508
+ * inherit the gate transitively through their declared dependencies. The
1509
+ * executor must stamp that GUC (transaction-scoped) before any statement
1510
+ * touching such a relation, or the synced arm yields zero rows regardless
1511
+ * of the compiled predicates.
1512
+ */
1513
+ claimsGated: z.boolean().default(false),
1514
+ columns: z.array(z.object({
1515
+ name: z.string(),
1516
+ columnName: z.string(),
1517
+ type: z.union([z.enum(COLUMN_TYPES), z.object({ enum: z.string(), values: z.array(z.string()) })]),
1518
+ nullable: z.boolean()
1519
+ })),
1520
+ /** Empty for keyless read-only views; keyed tables and views list logical columns. */
1521
+ primaryKey: z.array(z.string()),
1522
+ links: z.array(z.object({
1523
+ name: z.string(),
1524
+ kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
1525
+ target: z.string(),
1526
+ on: z.string(),
1527
+ references: z.string()
1528
+ })),
1529
+ /** §4f whitelists: what dynamic inputs may reference. */
1530
+ filterable: z.array(z.string()),
1531
+ orderable: z.array(z.string()),
1532
+ insertable: z.array(z.string()),
1533
+ settable: z.array(z.string()),
1534
+ appendable: z.array(z.string()),
1535
+ /**
1536
+ * Logical columns stamped with Postgres `now()` on UPDATE (canonical
1537
+ * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
1538
+ */
1539
+ touchOnUpdate: z.array(z.string()).optional(),
1540
+ /**
1541
+ * Per-cell metadata: logical column → the logical names of its cell-part
1542
+ * columns (which are listed in `columns`, but never filterable, orderable or
1543
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
1544
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
1545
+ * the relation tracks no cells.
1546
+ */
1547
+ cells: z.record(z.string(), z.object({
1548
+ at: z.string().optional(),
1549
+ by: z.string().optional(),
1550
+ rev: z.string().optional(),
1551
+ source: z.string().optional(),
1552
+ conflicts: z.string().optional()
1553
+ }).strict()).optional(),
1554
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
1555
+ resolved: resolvedMetaIrSchema.optional(),
1556
+ constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
1557
+ /** Depth/node caps for dynamic filter trees on this table. */
1558
+ dynamicFilterMaxDepth: z.number().int().positive().default(6),
1559
+ dynamicFilterMaxNodes: z.number().int().positive().default(64)
1560
+ });
1561
+ z.object({
1562
+ irVersion: irVersionSchema,
1563
+ namespace: z.string(),
1564
+ schemaHash: z.string(),
1565
+ opsHash: z.string(),
1566
+ tables: z.record(z.string(), tableMetaIrSchema),
1567
+ operations: z.record(z.string(), compiledOperationSchema)
1568
+ }).superRefine((artifact, ctx) => {
1569
+ for (const [name, operation] of Object.entries(artifact.operations)) {
1570
+ if (operation.irVersion > artifact.irVersion) {
1571
+ ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
1572
+ }
1573
+ }
1574
+ });
482
1575
 
483
1576
  // src/sdl/generate.ts
484
1577
  var SdlGenerationError = class extends Error {
@@ -501,7 +1594,9 @@ var PLATFORM_USER_FIELDS = [
501
1594
  ];
502
1595
  function generateSdl(canonical, options = {}) {
503
1596
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
504
- const tables = canonicalRelations(def).filter((table) => table.source === void 0);
1597
+ const exposed = canonicalRelations(def).filter((table) => table.source === void 0 && table.internal !== true);
1598
+ const cellsByTable = new Map(exposed.map((table) => [table.table, trackedCells(table)]));
1599
+ const tables = exposed.map((table) => ({ ...table, columns: table.columns.filter((column) => column.cell === void 0) }));
505
1600
  if (tables.length === 0) {
506
1601
  throw new SdlGenerationError(
507
1602
  `Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
@@ -557,8 +1652,14 @@ function generateSdl(canonical, options = {}) {
557
1652
  '"""Gates an operation behind a platform capability or custom role \u2014 checked by the data plane before execution."""\ndirective @requires(capability: String, role: String) on QUERY | MUTATION'
558
1653
  );
559
1654
  blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
1655
+ blocks.push(
1656
+ '"""Abort the entire mutation unless this root affects exactly the document-authored number of rows. Mutation roots only; cannot be conditional."""\ndirective @assertAffectedRows(exactly: Int!) on FIELD'
1657
+ );
1658
+ if (tables.some((table) => table.resolved !== void 0)) blocks.push(...RESOLVED_DIRECTIVE_BLOCKS);
560
1659
  for (const scalar of CUSTOM_SCALARS) blocks.push(`scalar ${scalar}`);
1660
+ blocks.push(...renderSharedCellTypes([...cellsByTable.values()]));
561
1661
  blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
1662
+ blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
562
1663
  for (const enumName of [...usedEnums].sort()) {
563
1664
  const entry = enumByName.get(enumName);
564
1665
  if (!entry) continue;
@@ -581,16 +1682,32 @@ function generateSdl(canonical, options = {}) {
581
1682
  names,
582
1683
  namesByTable,
583
1684
  emitAggregateBoolExp: aggregateBoolExpTargets.has(table.table),
584
- pinned: options.pinnedColumns?.[table.table] ?? []
1685
+ pinned: options.pinnedColumns?.[table.table] ?? [],
1686
+ cells: cellsByTable.get(table.table) ?? null
585
1687
  })
586
1688
  );
587
1689
  }
588
1690
  blocks.push(renderQueryRoot(tables, namesByTable));
589
1691
  const mutationRoot = renderMutationRoot(tables, namesByTable);
590
1692
  if (mutationRoot !== null) blocks.push(mutationRoot);
1693
+ assertNoDuplicateTypeNames(blocks);
591
1694
  return `${blocks.join("\n\n")}
592
1695
  `;
593
1696
  }
1697
+ var TYPE_DECLARATION_RE = /^(?:type|input|enum|scalar|interface|union)\s+([_A-Za-z][_0-9A-Za-z]*)/;
1698
+ function assertNoDuplicateTypeNames(blocks) {
1699
+ const seen = /* @__PURE__ */ new Set();
1700
+ for (const block of blocks) {
1701
+ const declared = TYPE_DECLARATION_RE.exec(block)?.[1];
1702
+ if (declared === void 0) continue;
1703
+ if (seen.has(declared)) {
1704
+ throw new SdlGenerationError(
1705
+ `Generated schema declares "${declared}" twice. A canonical enum or table name is PascalCased into this GraphQL type name and collides with another declaration (for example an enum named "date_bucket" renders as "DateBucket", the shared bucket enum). Rename the offending enum or table.`
1706
+ );
1707
+ }
1708
+ seen.add(declared);
1709
+ }
1710
+ }
594
1711
  function assertEnumValue(value, context) {
595
1712
  if (!GRAPHQL_NAME_RE.test(value) || RESERVED_ENUM_VALUES.has(value)) {
596
1713
  throw new SdlGenerationError(
@@ -656,6 +1773,52 @@ ${orderFields}
656
1773
  }`
657
1774
  ];
658
1775
  }
1776
+ var RESOLVED_DIRECTIVE_BLOCKS = [
1777
+ '"""Refresh this resolved-table root from a prefetch source before reading. Key-addressed roots only; the source must support the root shape."""\ndirective @prefetch(source: String!, maxStaleness: String) repeatable on FIELD',
1778
+ renderEnum("WritebackMode", ["SYNC", "ASYNC"]),
1779
+ '"""Write this resolved-table mutation back to a prefetch source (SYNC rolls back on failure; ASYNC queues)."""\ndirective @writeback(source: String!, mode: WritebackMode) repeatable on FIELD',
1780
+ '"""Declare that this mutation deliberately keeps writeback columns local to the named source."""\ndirective @localOnly(source: String!) repeatable on FIELD'
1781
+ ];
1782
+ function trackedCells(table) {
1783
+ const columns = [...new Set(table.columns.flatMap((column) => column.cell ? [column.cell.of] : []))];
1784
+ if (columns.length === 0) return null;
1785
+ return { resolved: table.columns.some((column) => column.cell?.part === "source"), columns };
1786
+ }
1787
+ function renderSharedCellTypes(all) {
1788
+ const present = all.filter((cells) => cells !== null);
1789
+ const blocks = [];
1790
+ if (present.some((cells) => !cells.resolved)) {
1791
+ blocks.push(`"""When, and by whom, a cell was last written."""
1792
+ type ${CELL_TYPENAMES.stamp} {
1793
+ at: timestamptz!
1794
+ by: String
1795
+ }`);
1796
+ }
1797
+ if (present.some((cells) => cells.resolved)) {
1798
+ blocks.push(`"""A newer candidate value that lost to the column's resolve rule."""
1799
+ type ${CELL_TYPENAMES.conflict} {
1800
+ source: String!
1801
+ at: timestamptz
1802
+ value: jsonb
1803
+ }`);
1804
+ blocks.push(`"""How a resolved cell was decided: the winning source, its time and actor, and newer losers."""
1805
+ type ${CELL_TYPENAMES.resolved} {
1806
+ source: String!
1807
+ at: timestamptz
1808
+ by: String
1809
+ conflicts: [${CELL_TYPENAMES.conflict}!]!
1810
+ }`);
1811
+ }
1812
+ return blocks;
1813
+ }
1814
+ function renderCellsType(ctx) {
1815
+ if (!ctx.cells) return [];
1816
+ const cellType = ctx.cells.resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp;
1817
+ const lines = ctx.cells.columns.map((column) => ` ${fieldName(column)}: ${cellType}`);
1818
+ return [`type ${cellsTypename(ctx.names.typename)} {
1819
+ ${lines.join("\n")}
1820
+ }`];
1821
+ }
659
1822
  function relationArgs(target) {
660
1823
  return `where: ${target.boolExp}, orderBy: [${target.orderBy}!], limit: Int, offset: Int`;
661
1824
  }
@@ -664,10 +1827,10 @@ function rootArgs(names) {
664
1827
  }
665
1828
  function primaryKeyColumns(table) {
666
1829
  if (!table.primaryKey || table.primaryKey.length === 0) return [];
667
- return table.primaryKey.map((identifier2) => {
668
- const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
1830
+ return table.primaryKey.map((identifier3) => {
1831
+ const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
669
1832
  if (!column) {
670
- throw new SdlGenerationError(`Primary key column "${identifier2}" not found on table "${table.table}".`);
1833
+ throw new SdlGenerationError(`Primary key column "${identifier3}" not found on table "${table.table}".`);
671
1834
  }
672
1835
  return column;
673
1836
  });
@@ -676,10 +1839,12 @@ function renderTableBlocks(ctx) {
676
1839
  const { table, names } = ctx;
677
1840
  const blocks = [
678
1841
  renderObjectType(ctx),
1842
+ ...renderCellsType(ctx),
679
1843
  renderSelectColumnEnum(ctx),
680
1844
  renderOrderByInput(ctx),
681
1845
  renderBoolExp(ctx),
682
- ...renderAggregateTypes(ctx)
1846
+ ...renderAggregateTypes(ctx),
1847
+ ...renderGroupByTypes(ctx)
683
1848
  ];
684
1849
  if (ctx.emitAggregateBoolExp) blocks.push(...renderAggregateBoolExp(ctx));
685
1850
  const pk = primaryKeyColumns(table);
@@ -689,11 +1854,49 @@ function renderTableBlocks(ctx) {
689
1854
  ${fields}
690
1855
  }`);
691
1856
  }
692
- if (allowsInsert(table) || allowsUpdate(table)) {
1857
+ if (ctx.table.resolved !== void 0) {
1858
+ blocks.push(...renderResolvedWriteBlocks(ctx));
1859
+ } else if (allowsInsert(table) || allowsUpdate(table)) {
693
1860
  blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
694
1861
  }
695
1862
  return blocks;
696
1863
  }
1864
+ function resolvedWritableColumns(table) {
1865
+ const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
1866
+ return table.columns.filter((column) => !system.has(column.name));
1867
+ }
1868
+ function renderResolvedWriteBlocks(ctx) {
1869
+ const { table, names } = ctx;
1870
+ const writable = resolvedWritableColumns(table);
1871
+ const pk = new Set(table.primaryKey ?? []);
1872
+ const settable = writable.filter((column) => !pk.has(column.columnName));
1873
+ const line = (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`;
1874
+ const blocks = [];
1875
+ if (allowsInsert(table)) blocks.push(`input ${names.insertInput} {
1876
+ ${writable.map(line).join("\n")}
1877
+ }`);
1878
+ if (allowsUpdate(table)) {
1879
+ blocks.push(`input ${names.setInput} {
1880
+ ${settable.map(line).join("\n")}
1881
+ }`);
1882
+ blocks.push(renderEnum(names.updateColumnEnum, settable.map((column) => fieldName(column.name))));
1883
+ }
1884
+ return blocks;
1885
+ }
1886
+ function renderResolvedMutationFields(table, names) {
1887
+ const lines = [];
1888
+ const pk = primaryKeyColumns(table);
1889
+ if (allowsInsert(table)) lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!): ${names.typename}`);
1890
+ if (allowsUpdate(table) && pk.length > 0) {
1891
+ lines.push(` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, _set: ${names.setInput}!): ${names.typename}`);
1892
+ lines.push(` ${names.revertByPkRoot}(pkColumns: ${names.pkColumnsInput}!, columns: [${names.updateColumnEnum}!]!): ${names.typename}`);
1893
+ if ((table.resolved?.deletedBy.length ?? 0) > 0) {
1894
+ const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
1895
+ lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
1896
+ }
1897
+ }
1898
+ return lines;
1899
+ }
697
1900
  function allowsInsert(table) {
698
1901
  return table.policies.write !== void 0 || table.policies.insert !== void 0;
699
1902
  }
@@ -716,6 +1919,10 @@ function renderObjectType(ctx) {
716
1919
  const deprecated = column.deprecated ? " @deprecated" : "";
717
1920
  lines.push(` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}${bang}${deprecated}`);
718
1921
  }
1922
+ if (ctx.cells) {
1923
+ lines.push(' """Per-cell metadata: when each field was last written, and by whom."""');
1924
+ lines.push(` ${CELLS_FIELD}: ${cellsTypename(names.typename)}!`);
1925
+ }
719
1926
  for (const link of table.links) {
720
1927
  if (link.description !== void 0) lines.push(...descriptionLines(link.description, " "));
721
1928
  if (link.kind === "toPlatformUser") {
@@ -784,7 +1991,10 @@ function numericishColumns(table) {
784
1991
  return table.columns.filter((column) => typeof column.type !== "object" && NUMERICISH.has(column.type));
785
1992
  }
786
1993
  function jsonbColumns(table) {
787
- return table.columns.filter((column) => column.type === "jsonb");
1994
+ return clientWritableColumns(table).filter((column) => column.type === "jsonb");
1995
+ }
1996
+ function clientWritableColumns(table) {
1997
+ return table.columns;
788
1998
  }
789
1999
  function renderAggregateTypes(ctx) {
790
2000
  const { table, names } = ctx;
@@ -816,6 +2026,32 @@ ${sumLines.join("\n")}
816
2026
  }
817
2027
  return blocks;
818
2028
  }
2029
+ function groupableColumns(table) {
2030
+ return table.columns.filter((column) => column.type !== "jsonb");
2031
+ }
2032
+ function renderGroupByTypes(ctx) {
2033
+ const { table, names } = ctx;
2034
+ const groupable = groupableColumns(table);
2035
+ if (groupable.length === 0) return [];
2036
+ const keyLines = groupable.map((column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`);
2037
+ return [
2038
+ renderEnum(names.groupByColumnEnum, groupable.map((column) => fieldName(column.name))),
2039
+ `input ${names.groupByInput} {
2040
+ column: ${names.groupByColumnEnum}!
2041
+ bucket: ${DATE_BUCKET_ENUM}
2042
+ }`,
2043
+ `type ${names.groupKey} {
2044
+ ${keyLines.join("\n")}
2045
+ }`,
2046
+ `type ${names.aggregateGroup} {
2047
+ key: ${names.groupKey}!
2048
+ aggregate: ${names.aggregateFields}
2049
+ }`
2050
+ ];
2051
+ }
2052
+ function groupRootArgs(names) {
2053
+ return `where: ${names.boolExp}, groupBy: [${names.groupByInput}!]!, limit: Int, offset: Int`;
2054
+ }
819
2055
  function renderAggregateBoolExp(ctx) {
820
2056
  const { names } = ctx;
821
2057
  return [
@@ -841,12 +2077,15 @@ function constraintValues(table) {
841
2077
  }
842
2078
  return [...values].sort();
843
2079
  }
2080
+ function clientSettableColumns(table) {
2081
+ return clientWritableColumns(table).filter((column) => column.onUpdateFn === void 0);
2082
+ }
844
2083
  function renderWriteBlocks(ctx, access) {
845
2084
  const { table, names, pinned } = ctx;
846
2085
  const blocks = [];
847
2086
  if (access.insert) {
848
2087
  const pinnedSet = new Set(pinned);
849
- const insertable = table.columns.filter(
2088
+ const insertable = clientWritableColumns(table).filter(
850
2089
  (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
851
2090
  );
852
2091
  if (insertable.length === 0) {
@@ -862,7 +2101,7 @@ ${insertLines.join("\n")}
862
2101
  }`);
863
2102
  }
864
2103
  if (access.update) {
865
- const setLines = table.columns.map(
2104
+ const setLines = clientSettableColumns(table).map(
866
2105
  (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
867
2106
  );
868
2107
  blocks.push(`input ${names.setInput} {
@@ -889,7 +2128,7 @@ ${incLines.join("\n")}
889
2128
  if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
890
2129
  for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
891
2130
  blocks.push(renderEnum(names.constraintEnum, constraints));
892
- const updateColumns = table.columns.map((column) => fieldName(column.name));
2131
+ const updateColumns = clientSettableColumns(table).map((column) => fieldName(column.name));
893
2132
  blocks.push(renderEnum(names.updateColumnEnum, updateColumns));
894
2133
  blocks.push(
895
2134
  `input ${names.onConflict} {
@@ -921,6 +2160,9 @@ function renderQueryRoot(tables, namesByTable) {
921
2160
  lines.push(` ${names.byPkRoot}(${args}): ${names.typename}`);
922
2161
  }
923
2162
  lines.push(` ${names.aggregateRoot}(${rootArgs(names)}): ${names.aggregate}!`);
2163
+ if (groupableColumns(table).length > 0) {
2164
+ lines.push(` ${names.aggregateGroupsRoot}(${groupRootArgs(names)}): [${names.aggregateGroup}!]!`);
2165
+ }
924
2166
  }
925
2167
  lines.push("}");
926
2168
  return lines.join("\n");
@@ -936,6 +2178,10 @@ function renderMutationRoot(tables, namesByTable) {
936
2178
  if (!canInsert && !canUpdate) continue;
937
2179
  const names = namesByTable.get(table.table);
938
2180
  if (!names) continue;
2181
+ if (table.resolved !== void 0) {
2182
+ lines.push(...renderResolvedMutationFields(table, names));
2183
+ continue;
2184
+ }
939
2185
  const constraints = constraintValues(table);
940
2186
  const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
941
2187
  const updateArgs = [`_set: ${names.setInput}`];
@@ -1069,13 +2315,13 @@ function generateTypePolicies(canonical) {
1069
2315
  const entries = [];
1070
2316
  let hasPlatformUser = false;
1071
2317
  for (const table of canonicalRelations(def)) {
1072
- if (table.source !== void 0) continue;
2318
+ if (table.source !== void 0 || table.internal === true) continue;
1073
2319
  if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
1074
2320
  if (!table.primaryKey || table.primaryKey.length === 0) continue;
1075
- const keyFields = table.primaryKey.map((identifier2) => {
1076
- const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
2321
+ const keyFields = table.primaryKey.map((identifier3) => {
2322
+ const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
1077
2323
  if (!column) {
1078
- throw new CodegenGqlError(`Primary key column "${identifier2}" not found on relation "${table.table}".`);
2324
+ throw new CodegenGqlError(`Primary key column "${identifier3}" not found on relation "${table.table}".`);
1079
2325
  }
1080
2326
  return fieldName(column.name);
1081
2327
  });
@@ -1093,6 +2339,6 @@ ${body}
1093
2339
  `;
1094
2340
  }
1095
2341
 
1096
- export { CUSTOM_SCALARS, MANIFEST_VERSION, NUMERICISH_COLUMN_TYPES, ORDER_BY_ENUM, ORDER_BY_ENUM_VALUES, PLATFORM_USER_TYPENAME, SdlGenerationError, VARIABLE_SCALAR_NAMES, buildOperationsManifest, camelCase, columnTypeForScalar, comparisonExpName, enumTypeName, fieldName, generateSdl, generateTypePolicies, graphqlScalarFor, manifestOperationSchema, manifestRequiresSchema, operationsManifestSchema, pascalCase, tableNames, typeName };
2342
+ export { BUCKETABLE_COLUMN_TYPES, CELL_TYPENAMES, CUSTOM_SCALARS, DATE_BUCKET_ENUM, MANIFEST_VERSION, NUMERICISH_COLUMN_TYPES, ORDER_BY_ENUM, ORDER_BY_ENUM_VALUES, PLATFORM_USER_TYPENAME, SdlGenerationError, VARIABLE_SCALAR_NAMES, buildOperationsManifest, camelCase, cellsTypename, columnTypeForScalar, comparisonExpName, enumTypeName, fieldName, generateSdl, generateTypePolicies, graphqlScalarFor, manifestOperationSchema, manifestRequiresSchema, operationsManifestSchema, pascalCase, resolvedWritableColumns, tableNames, typeName };
1097
2343
  //# sourceMappingURL=sdl.js.map
1098
2344
  //# sourceMappingURL=sdl.js.map