@sequenceholdings/orm 2.1.0 → 2.2.0

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 (77) hide show
  1. package/README.md +40 -1
  2. package/dist/authoring.cjs +1619 -121
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +252 -8
  5. package/dist/authoring.d.ts +252 -8
  6. package/dist/authoring.js +1607 -123
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +713 -41
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +1520 -51
  11. package/dist/canonical.d.ts +1520 -51
  12. package/dist/canonical.js +693 -42
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +7103 -3901
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +7104 -3902
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs +122 -40
  19. package/dist/client.cjs.map +1 -1
  20. package/dist/client.d.cts +85 -25
  21. package/dist/client.d.ts +85 -25
  22. package/dist/client.js +119 -41
  23. package/dist/client.js.map +1 -1
  24. package/dist/compiler.cjs +2464 -450
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +79 -4
  27. package/dist/compiler.d.ts +79 -4
  28. package/dist/compiler.js +2457 -451
  29. package/dist/compiler.js.map +1 -1
  30. package/dist/constants-C6EG6Wq7.d.cts +99 -0
  31. package/dist/constants-C6EG6Wq7.d.ts +99 -0
  32. package/dist/definition-DjfC7Lll.d.cts +91 -0
  33. package/dist/definition-DjfC7Lll.d.ts +91 -0
  34. package/dist/dynamic-input.cjs +2 -1
  35. package/dist/dynamic-input.cjs.map +1 -1
  36. package/dist/dynamic-input.d.cts +9 -1
  37. package/dist/dynamic-input.d.ts +9 -1
  38. package/dist/dynamic-input.js +2 -1
  39. package/dist/dynamic-input.js.map +1 -1
  40. package/dist/index.cjs +2656 -418
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +693 -116
  43. package/dist/index.d.ts +693 -116
  44. package/dist/index.js +2622 -420
  45. package/dist/index.js.map +1 -1
  46. package/dist/ir.cjs +811 -48
  47. package/dist/ir.cjs.map +1 -1
  48. package/dist/ir.d.cts +670 -27
  49. package/dist/ir.d.ts +670 -27
  50. package/dist/ir.js +802 -49
  51. package/dist/ir.js.map +1 -1
  52. package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
  53. package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
  54. package/dist/policy-1iZbGnZZ.d.cts +24 -0
  55. package/dist/policy-1iZbGnZZ.d.ts +24 -0
  56. package/dist/predicate-v2.cjs +804 -51
  57. package/dist/predicate-v2.cjs.map +1 -1
  58. package/dist/predicate-v2.d.cts +2 -1
  59. package/dist/predicate-v2.d.ts +2 -1
  60. package/dist/predicate-v2.js +804 -51
  61. package/dist/predicate-v2.js.map +1 -1
  62. package/dist/sdl.cjs +1014 -75
  63. package/dist/sdl.cjs.map +1 -1
  64. package/dist/sdl.d.cts +14 -3
  65. package/dist/sdl.d.ts +14 -3
  66. package/dist/sdl.js +1012 -76
  67. package/dist/sdl.js.map +1 -1
  68. package/package.json +13 -7
  69. package/src/codegen-gql/server-fns.cjs +2 -2
  70. package/templates/orm-namespace/AGENTS.md +10 -0
  71. package/templates/orm-namespace/CLAUDE.md +1 -82
  72. package/templates/orm-namespace/codegen.preset.d.cts +7 -0
  73. package/templates/orm-namespace/codegen.ts +2 -1
  74. package/templates/orm-namespace/graphql/example/queries.ts +4 -3
  75. package/templates/orm-namespace/package.json +2 -1
  76. package/templates/orm-namespace/schema/tables.ts +3 -2
  77. package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/ir.cjs CHANGED
@@ -3,14 +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
+ }
6
204
 
7
205
  // src/constants.ts
8
206
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
207
+ var MAX_IDENTIFIER_LENGTH = 63;
208
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
209
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
210
+ var CELL_PART_SUFFIX = {
211
+ at: "__at",
212
+ by: "__by",
213
+ rev: "__rev",
214
+ source: "__src",
215
+ conflicts: "__lost"
216
+ };
217
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
218
+ var CELLS_FIELD = "cells";
219
+ function cellColumnName(column, part) {
220
+ return `${column}${CELL_PART_SUFFIX[part]}`;
221
+ }
222
+ var MATERIALIZE_ACTOR = "orm:materialize";
223
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
224
+ var policyVariantSchema = zod.z.object({
225
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
226
+ when: zod.z.string().min(1).max(1e3),
227
+ /** Exactly one of: allow-all bypass, or a row predicate. */
228
+ allow: zod.z.literal(true).optional(),
229
+ policy: zod.z.string().min(1).max(2e3).optional()
230
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
231
+ message: "A variant declares exactly one of allow: true or policy"
232
+ });
233
+ var policyExpressionSchema = zod.z.union([
234
+ zod.z.string().min(1).max(2e3),
235
+ zod.z.object({
236
+ default: zod.z.string().min(1).max(2e3),
237
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
238
+ })
239
+ ]);
240
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
241
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
242
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
243
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
244
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
245
+ };
246
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
247
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
248
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
249
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
250
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
251
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
252
+ };
253
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
254
+ /* @__PURE__ */ new Set([
255
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
256
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
257
+ ]);
258
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
259
+ var canonicalFunctionRefSchema = zod.z.object({
260
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
261
+ kind: zod.z.enum(["managed", "native"]),
262
+ id: zod.z.string().min(1).max(200)
263
+ }).strict();
264
+ var canonicalFetchShapeSchema = zod.z.union([
265
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
266
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
267
+ ]);
268
+ var canonicalResolvedSourceSchema = zod.z.object({
269
+ name: identifier,
270
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
271
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
272
+ table: identifier,
273
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
274
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
275
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
276
+ updatedAt: zod.z.string().min(1).max(63).optional(),
277
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
278
+ key: zod.z.string().min(1).max(63).optional(),
279
+ fetch: canonicalFetchShapeSchema.optional(),
280
+ function: canonicalFunctionRefSchema.optional(),
281
+ prefetchDefaults: zod.z.object({
282
+ maxStalenessMs: durationMs,
283
+ timeoutMs: durationMs,
284
+ onError: zod.z.enum(["fallback", "fail"])
285
+ }).strict().optional(),
286
+ writeback: zod.z.object({
287
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
288
+ defaultMode: zod.z.enum(["sync", "async"]),
289
+ timeoutMs: durationMs,
290
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
291
+ clearManualOnConfirm: zod.z.boolean()
292
+ }).strict().optional()
293
+ }).strict();
294
+ var canonicalResolveRuleSchema = zod.z.object({
295
+ column: zod.z.string().min(1).max(63),
296
+ comparator: zod.z.enum(COMPARATORS),
297
+ /** Source names, in tie-break order. */
298
+ candidates: zod.z.array(identifier).min(1).max(16),
299
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
300
+ orderBy: zod.z.string().min(1).max(2e3).optional()
301
+ }).strict();
302
+ var canonicalResolvedSchema = zod.z.object({
303
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
304
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
305
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
306
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
307
+ deletedBy: zod.z.array(identifier).max(1).default([]),
308
+ /**
309
+ * Materialized: the view reads an indexed internal table (`table`) that the
310
+ * data plane keeps in step with `compute`, the internal view holding the
311
+ * resolution query. Omitted when the view computes on read.
312
+ */
313
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
314
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
315
+ policies: zod.z.object({
316
+ write: policyExpressionSchema.optional(),
317
+ insert: policyExpressionSchema.optional(),
318
+ update: policyExpressionSchema.optional()
319
+ }).strict().default({})
320
+ }).strict();
321
+ function validateResolvedView({ view, tables, views = [] }) {
322
+ const resolved = view.resolved;
323
+ if (!resolved) return [];
324
+ const errors = [];
325
+ const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
326
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
327
+ if (!pkColumn) {
328
+ fail("requires exactly one primary-key column");
329
+ return errors;
330
+ }
331
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
332
+ const scope = {
333
+ view,
334
+ resolved,
335
+ pkColumn,
336
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
337
+ sourceByName: /* @__PURE__ */ new Map(),
338
+ fail
339
+ };
340
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
341
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
342
+ for (const source of resolved.sources) {
343
+ if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
344
+ scope.sourceByName.set(source.name, source);
345
+ const table = tableByName.get(source.table);
346
+ if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
347
+ else validateSource({ scope, source, table });
348
+ }
349
+ validateRules(scope);
350
+ validateViewCells(scope);
351
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
352
+ for (const name of resolved.deletedBy) {
353
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
354
+ }
355
+ return errors;
356
+ }
357
+ function validateSource({ scope, source, table }) {
358
+ const { fail } = scope;
359
+ if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
360
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
361
+ fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
362
+ }
363
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
364
+ if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
365
+ if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
366
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
367
+ validateSourceColumns({ scope, source, byName });
368
+ if (source.kind === "etl") {
369
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
370
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
371
+ fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
372
+ }
373
+ }
374
+ validatePrefetchFields({ scope, source });
375
+ }
376
+ function validatePrefetchFields({ scope, source }) {
377
+ const { fail, pkColumn } = scope;
378
+ if (source.kind !== "prefetch") {
379
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
380
+ fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
381
+ }
382
+ return;
383
+ }
384
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
385
+ fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
386
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
387
+ fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
388
+ }
389
+ for (const column of source.writeback?.columns ?? []) {
390
+ if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
391
+ }
392
+ }
393
+ function validateSourceColumns({ scope, source, byName }) {
394
+ const { fail, pkColumn, dataColumns } = scope;
395
+ for (const column of [pkColumn.name, ...source.columns]) {
396
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
397
+ const theirs = byName.get(column);
398
+ if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
399
+ else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
400
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
401
+ }
402
+ }
403
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
404
+ const ruled = /* @__PURE__ */ new Set();
405
+ for (const rule of resolved.resolve) {
406
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
407
+ fail(`resolve rule targets unknown column '${rule.column}'`);
408
+ continue;
409
+ }
410
+ if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
411
+ ruled.add(rule.column);
412
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
413
+ fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
414
+ }
415
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
416
+ for (const candidate of rule.candidates) {
417
+ const source = sourceByName.get(candidate);
418
+ if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
419
+ else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
420
+ }
421
+ }
422
+ for (const column of dataColumns.keys()) {
423
+ if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
424
+ }
425
+ }
426
+ var VIEW_CELL_TYPES = {
427
+ source: "text",
428
+ at: "timestamptz",
429
+ by: "text",
430
+ conflicts: "jsonb"
431
+ };
432
+ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
433
+ const expected = /* @__PURE__ */ new Map();
434
+ for (const column of dataColumns.values()) {
435
+ if (column.name === pkColumn.name) continue;
436
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
437
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
438
+ }
439
+ }
440
+ for (const column of view.columns) {
441
+ if (!column.cell) continue;
442
+ const want = expected.get(column.name);
443
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
444
+ fail(`unexpected cell column '${column.name}'`);
445
+ continue;
446
+ }
447
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
448
+ expected.delete(column.name);
449
+ }
450
+ for (const name of expected.keys()) fail(`missing cell column '${name}'`);
451
+ }
452
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
453
+ function validateMaterialized({ scope, tableByName, views }) {
454
+ const { view, resolved, fail } = scope;
455
+ const { table: tableName, compute: computeName } = resolved.materialized;
456
+ const table = tableByName.get(tableName);
457
+ if (!table || table.internal !== true || table.materializes !== view.view) {
458
+ fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
459
+ } else {
460
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
461
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
462
+ }
463
+ const compute = views.find((candidate) => candidate.view === computeName);
464
+ if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
465
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
466
+ for (const source of resolved.sources) {
467
+ if (source.kind !== "etl") continue;
468
+ const read = tableByName.get(source.table)?.policies?.read;
469
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
470
+ if (!unconditional) {
471
+ fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
472
+ }
473
+ }
474
+ }
9
475
 
10
476
  // src/canonical/v2.ts
11
477
  var CANONICAL_FORMAT = 2;
12
478
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
13
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
479
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
14
480
  var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
15
481
  var COLUMN_TYPES = [
16
482
  "uuid",
@@ -26,14 +492,14 @@ var COLUMN_TYPES = [
26
492
  ];
27
493
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
28
494
  var canonicalEnumSchema = zod.z.object({
29
- name: identifier,
495
+ name: identifier2,
30
496
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
31
497
  });
32
498
  var canonicalColumnSchema = zod.z.object({
33
499
  /** Property name in TS (camelCase); columnName is the Postgres name. */
34
500
  name: zod.z.string().min(1).max(63),
35
501
  columnName: columnIdentifier,
36
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
502
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
37
503
  nullable: zod.z.boolean().default(true),
38
504
  unique: zod.z.boolean().default(false),
39
505
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -49,7 +515,14 @@ var canonicalColumnSchema = zod.z.object({
49
515
  /** Migration hints — same semantics as v1. */
50
516
  renamedFrom: columnIdentifier.optional(),
51
517
  cast: zod.z.string().max(200).optional(),
52
- deprecated: zod.z.boolean().default(false)
518
+ deprecated: zod.z.boolean().default(false),
519
+ /**
520
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
521
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
522
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
523
+ * ordinary columns so their hashes are unchanged.
524
+ */
525
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
53
526
  }).superRefine((column, ctx) => {
54
527
  if (column.onUpdateFn === void 0) return;
55
528
  if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
@@ -64,36 +537,28 @@ var canonicalLinkSchema = zod.z.object({
64
537
  name: zod.z.string().min(1).max(63),
65
538
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
66
539
  /** Target table name (namespace-local). Absent for toPlatformUser. */
67
- target: identifier.optional(),
540
+ target: identifier2.optional(),
541
+ /** Owning namespace of an imported target; omitted for local links. */
542
+ namespace: identifier2.max(41).optional(),
68
543
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
69
544
  on: columnIdentifier,
70
545
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
71
546
  references: columnIdentifier.optional(),
72
547
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
73
548
  enforce: zod.z.boolean().default(false),
549
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
550
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
74
551
  description: zod.z.string().max(500).optional()
75
552
  });
76
553
  var canonicalIndexSchema = zod.z.object({
77
554
  columns: zod.z.array(columnIdentifier).min(1),
78
555
  unique: zod.z.boolean().default(false),
79
- name: identifier.optional()
80
- });
81
- var policyVariantSchema = zod.z.object({
82
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
83
- when: zod.z.string().min(1).max(1e3),
84
- /** Exactly one of: allow-all bypass, or a row predicate. */
85
- allow: zod.z.literal(true).optional(),
86
- policy: zod.z.string().min(1).max(2e3).optional()
87
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
88
- message: "A variant declares exactly one of allow: true or policy"
556
+ name: identifier2.optional()
89
557
  });
90
- var policyExpressionSchema = zod.z.union([
91
- zod.z.string().min(1).max(2e3),
92
- zod.z.object({
93
- default: zod.z.string().min(1).max(2e3),
94
- variants: zod.z.array(policyVariantSchema).max(10).default([])
95
- })
96
- ]);
558
+ var canonicalCheckSchema = zod.z.object({
559
+ name: identifier2,
560
+ expression: zod.z.string().min(1).max(2e3)
561
+ }).strict();
97
562
  var canonicalPoliciesSchema = zod.z.object({
98
563
  read: policyExpressionSchema.optional(),
99
564
  write: policyExpressionSchema.optional(),
@@ -124,7 +589,7 @@ function isSingleViewQuerySql(value) {
124
589
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
125
590
  }
126
591
  var canonicalActionSchema = zod.z.object({
127
- name: identifier,
592
+ name: identifier2,
128
593
  requires: zod.z.string().max(1e3).optional(),
129
594
  params: zod.z.record(zod.z.string(), zod.z.object({
130
595
  type: columnTypeSchema,
@@ -136,9 +601,9 @@ var canonicalActionSchema = zod.z.object({
136
601
  });
137
602
  var canonicalCowMutationSchema = zod.z.object({
138
603
  kind: zod.z.enum(["override", "revert"]),
139
- name: identifier,
604
+ name: identifier2,
140
605
  /** Synced table (same namespace) the overlay copies from / falls back to. */
141
- synced: identifier,
606
+ synced: identifier2,
142
607
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
143
608
  pk: zod.z.string().min(1).max(63),
144
609
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -166,23 +631,28 @@ var canonicalCowMutationSchema = zod.z.object({
166
631
  }
167
632
  });
168
633
  var canonicalForeignKeySchema = zod.z.object({
169
- name: identifier,
634
+ name: identifier2,
170
635
  columns: zod.z.array(columnIdentifier).min(1).max(8),
171
- target: identifier,
636
+ target: identifier2,
172
637
  references: zod.z.array(columnIdentifier).min(1).max(8),
173
- deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
638
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
639
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
640
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
174
641
  }).strict();
175
642
  var canonicalTableSchema = zod.z.object({
176
- table: identifier,
643
+ table: identifier2,
644
+ displayName: zod.z.string().trim().min(1).max(120).optional(),
177
645
  description: zod.z.string().max(2e3).optional(),
178
646
  /** Table-level rename hint (new in v2). */
179
- renamedFrom: identifier.optional(),
180
- /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
181
- source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
647
+ renamedFrom: identifier2.optional(),
648
+ /** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
649
+ source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
182
650
  columns: zod.z.array(canonicalColumnSchema).default([]),
183
651
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
184
652
  links: zod.z.array(canonicalLinkSchema).default([]),
185
653
  indexes: zod.z.array(canonicalIndexSchema).default([]),
654
+ /** Omitted by old definitions, preserving their existing content hashes. */
655
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
186
656
  policies: canonicalPoliciesSchema.default({}),
187
657
  /**
188
658
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -193,22 +663,55 @@ var canonicalTableSchema = zod.z.object({
193
663
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
194
664
  cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
195
665
  /** Omitted by old definitions, preserving their existing content hashes. */
196
- foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
666
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
667
+ /**
668
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
669
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
670
+ * on a resolved table's internal sources), listed explicitly in `columns`
671
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
672
+ * Omitted when off so existing definitions hash unchanged.
673
+ */
674
+ cellMetadata: zod.z.literal(true).optional(),
675
+ /**
676
+ * System-owned backing table of a resolved table (its manual or prefetch
677
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
678
+ * only the data plane writes them. Omitted when false.
679
+ */
680
+ internal: zod.z.literal(true).optional(),
681
+ /**
682
+ * The materialized copy of resolved table `materializes` (internal): the
683
+ * same columns as that view, including its cell parts, maintained by the
684
+ * data plane. Omitted otherwise.
685
+ */
686
+ materializes: identifier2.optional()
197
687
  });
688
+ var MAX_VIEW_QUERY = 1e5;
689
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
198
690
  var canonicalViewSchema = zod.z.object({
199
- view: identifier,
691
+ view: identifier2,
200
692
  description: zod.z.string().max(2e3).optional(),
201
693
  columns: zod.z.array(canonicalColumnSchema).min(1),
202
694
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
203
695
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
204
696
  links: zod.z.array(canonicalLinkSchema).default([]),
205
697
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
206
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
698
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
207
699
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
208
700
  }),
209
701
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
210
- dependencies: zod.z.array(identifier).max(500)
702
+ dependencies: zod.z.array(identifier2).max(500),
703
+ /**
704
+ * Present when this view is a resolved table (defineResolvedTable): the
705
+ * multi-source metadata behind its generated query. Omitted otherwise so
706
+ * existing views hash unchanged.
707
+ */
708
+ resolved: canonicalResolvedSchema.optional(),
709
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
710
+ internal: zod.z.literal(true).optional()
211
711
  }).superRefine((view, ctx) => {
712
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
713
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
714
+ }
212
715
  for (const [index, column] of view.columns.entries()) {
213
716
  if (column.default !== void 0 || column.defaultFn !== void 0) {
214
717
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
@@ -223,20 +726,91 @@ var canonicalViewSchema = zod.z.object({
223
726
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
224
727
  }
225
728
  }
729
+ if (view.resolved === void 0 && view.internal !== true) {
730
+ for (const [index, column] of view.columns.entries()) {
731
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
732
+ }
733
+ }
226
734
  for (const [index, link] of view.links.entries()) {
227
735
  if (link.enforce) {
228
736
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
229
737
  }
738
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
739
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
740
+ }
230
741
  }
231
742
  });
232
- zod.z.object({
743
+ var CELL_PART_TYPES = {
744
+ at: "timestamptz",
745
+ by: "text",
746
+ rev: "uuid",
747
+ source: "text",
748
+ conflicts: "jsonb"
749
+ };
750
+ function trackedCellColumns(table) {
751
+ const pk = new Set(table.primaryKey ?? []);
752
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
753
+ }
754
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
755
+ function validateCellMetadata(table, index, ctx) {
756
+ const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
757
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
758
+ if (table.materializes !== void 0) return;
759
+ if (table.cellMetadata !== true) {
760
+ for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
761
+ return;
762
+ }
763
+ if (table.source !== void 0) {
764
+ fail(`synced table '${table.table}' cannot track cell metadata`);
765
+ return;
766
+ }
767
+ const tracked = trackedCellColumns(table);
768
+ const byName = new Map(tracked.map((column) => [column.name, column]));
769
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
770
+ if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
771
+ const seen = /* @__PURE__ */ new Set();
772
+ for (const { column, position, cell } of cells) {
773
+ const path = ["tables", index, "columns", position];
774
+ const owner = byName.get(cell.of);
775
+ if (!owner) {
776
+ fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
777
+ continue;
778
+ }
779
+ 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);
780
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
781
+ fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
782
+ }
783
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
784
+ fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
785
+ }
786
+ const key = `${cell.of}:${cell.part}`;
787
+ if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
788
+ seen.add(key);
789
+ }
790
+ for (const column of tracked) {
791
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
792
+ fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
793
+ }
794
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
795
+ fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
796
+ }
797
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
798
+ if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
799
+ }
800
+ }
801
+ }
802
+ var exportGateSchema = zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string().min(1) }).strict(), zod.z.object({ role: zod.z.string().min(1) }).strict()]);
803
+ var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
804
+ var localDefinitionSchema = zod.z.object({
233
805
  definition_format: zod.z.literal(CANONICAL_FORMAT),
234
- namespace: identifier.max(41),
806
+ namespace: identifier2.max(41),
235
807
  description: zod.z.string().max(2e3).optional(),
236
808
  enums: zod.z.array(canonicalEnumSchema).default([]),
237
809
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
238
810
  /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
239
811
  views: zod.z.array(canonicalViewSchema).max(500).optional(),
812
+ /** Target-owned cross-namespace access contract, using existing namespace grants. */
813
+ exports: zod.z.record(identifier2, relationExportSchema).optional(),
240
814
  /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
241
815
  capabilities: zod.z.object({
242
816
  roles: zod.z.unknown().optional(),
@@ -249,6 +823,7 @@ zod.z.object({
249
823
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
250
824
  }
251
825
  seen.add(table.table);
826
+ validateCellMetadata(table, index, ctx);
252
827
  if (table.source !== void 0) {
253
828
  for (const [columnIndex, column] of table.columns.entries()) {
254
829
  if (column.onUpdateFn !== void 0) {
@@ -262,12 +837,41 @@ zod.z.object({
262
837
  }
263
838
  }
264
839
  const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
840
+ const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
841
+ for (const name of Object.keys(definition.exports ?? {})) {
842
+ if (!relations.has(name)) ctx.addIssue({ code: "custom", path: ["exports", name], message: `export '${name}' is not a local object` });
843
+ }
265
844
  for (const [tableIndex, table] of definition.tables.entries()) {
845
+ for (const [linkIndex, link] of table.links.entries()) {
846
+ if (link.enforce && link.namespace !== void 0 && link.namespace !== definition.namespace) {
847
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", linkIndex], message: "physical foreign keys require a local target; use an ORM link without enforce" });
848
+ }
849
+ }
266
850
  const names = /* @__PURE__ */ new Set([
267
851
  ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
268
852
  ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
269
853
  ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
270
854
  ]);
855
+ for (const [index, link] of table.links.entries()) {
856
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
857
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
858
+ }
859
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
860
+ const source = table.columns.find((column) => column.columnName === link.on);
861
+ 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" });
862
+ }
863
+ }
864
+ for (const [index, check] of (table.checks ?? []).entries()) {
865
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
866
+ if (table.source) fail("synced tables cannot declare checks");
867
+ if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
868
+ names.add(check.name);
869
+ try {
870
+ compileCheckExpression(check.expression, table.columns);
871
+ } catch (error) {
872
+ fail(error instanceof Error ? error.message : String(error));
873
+ }
874
+ }
271
875
  for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
272
876
  const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
273
877
  if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
@@ -285,10 +889,18 @@ zod.z.object({
285
889
  if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
286
890
  else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
287
891
  }
892
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
893
+ 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");
894
+ }
288
895
  const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
289
896
  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");
290
897
  }
291
898
  }
899
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
900
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
901
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
902
+ }
903
+ }
292
904
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
293
905
  for (const [tableIndex, table] of definition.tables.entries()) {
294
906
  const cowMutations = table.cowMutations ?? [];
@@ -404,11 +1016,19 @@ zod.z.object({
404
1016
  };
405
1017
  for (const view of views) visit(view.view, []);
406
1018
  });
1019
+ localDefinitionSchema.safeExtend({
1020
+ /** Flattened, pinned snapshots. Imported relations never participate in this namespace's DDL. */
1021
+ imports: zod.z.array(localDefinitionSchema).max(32).optional()
1022
+ });
407
1023
 
408
1024
  // src/ir/index.ts
409
- var IR_VERSION = 2;
1025
+ var IR_VERSION = 5;
410
1026
  var LEGACY_IR_VERSION = 1;
411
- var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
1027
+ var GUARDED_IR_VERSION = 2;
1028
+ var COMPOSED_IR_VERSION = 3;
1029
+ var SQL_IR_VERSION = 4;
1030
+ var RUNTIME_SQL_IR_VERSION = 5;
1031
+ var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(GUARDED_IR_VERSION), zod.z.literal(COMPOSED_IR_VERSION), zod.z.literal(SQL_IR_VERSION), zod.z.literal(RUNTIME_SQL_IR_VERSION)]);
412
1032
  var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
413
1033
  function requiredArtifactIrVersion(operations) {
414
1034
  let required = LEGACY_IR_VERSION;
@@ -460,8 +1080,8 @@ var predIrSchema = zod.z.lazy(
460
1080
  value: zod.z.number(),
461
1081
  where: predIrSchema.optional()
462
1082
  }),
463
- zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string() }),
464
- zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string() }),
1083
+ zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string(), namespace: zod.z.string().optional() }),
1084
+ zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string(), namespace: zod.z.string().optional() }),
465
1085
  zod.z.object({ kind: zod.z.literal("dynamicFilter"), slot: zod.z.string() }),
466
1086
  zod.z.object({ kind: zod.z.literal("const"), value: zod.z.boolean() })
467
1087
  ])
@@ -489,6 +1109,21 @@ var selectionIrSchema = zod.z.lazy(
489
1109
  () => zod.z.object({
490
1110
  typename: zod.z.string(),
491
1111
  columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
1112
+ cells: zod.z.object({
1113
+ alias: zod.z.string(),
1114
+ typename: zod.z.string(),
1115
+ fields: zod.z.array(zod.z.object({
1116
+ alias: zod.z.string(),
1117
+ column: zod.z.string(),
1118
+ typename: zod.z.string(),
1119
+ parts: zod.z.array(zod.z.object({
1120
+ alias: zod.z.string(),
1121
+ part: zod.z.enum(["source", "at", "by", "conflicts"]),
1122
+ fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
1123
+ typename: zod.z.string().optional()
1124
+ }))
1125
+ }))
1126
+ }).optional(),
492
1127
  relations: zod.z.array(zod.z.object({
493
1128
  alias: zod.z.string(),
494
1129
  link: zod.z.string(),
@@ -510,6 +1145,11 @@ var selectionIrSchema = zod.z.lazy(
510
1145
  })
511
1146
  );
512
1147
  var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
1148
+ var prefetchPlanIrSchema = zod.z.object({
1149
+ source: zod.z.string(),
1150
+ maxStalenessMs: zod.z.number().int().positive().optional(),
1151
+ key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
1152
+ });
513
1153
  var selectRootSchema = zod.z.object({
514
1154
  kind: zod.z.literal("select"),
515
1155
  alias: zod.z.string(),
@@ -521,7 +1161,8 @@ var selectRootSchema = zod.z.object({
521
1161
  distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
522
1162
  limit: limitIrSchema,
523
1163
  offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
524
- includeIf: includeIfSchema.optional()
1164
+ includeIf: includeIfSchema.optional(),
1165
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
525
1166
  });
526
1167
  var byPkRootSchema = zod.z.object({
527
1168
  kind: zod.z.literal("byPk"),
@@ -531,7 +1172,8 @@ var byPkRootSchema = zod.z.object({
531
1172
  pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
532
1173
  /** Policy predicate — ANDed with the PK match. */
533
1174
  where: predIrSchema,
534
- includeIf: includeIfSchema.optional()
1175
+ includeIf: includeIfSchema.optional(),
1176
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
535
1177
  });
536
1178
  var aggregateRootSchema = zod.z.object({
537
1179
  kind: zod.z.literal("aggregate"),
@@ -589,7 +1231,20 @@ var mutationRootSchema = zod.z.object({
589
1231
  * coalesce view falls back to synced. Both are gated on a matching synced
590
1232
  * row and carry the write policy in `where` like update/delete.
591
1233
  */
592
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
1234
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
1235
+ /**
1236
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
1237
+ * executor gates on the resolved row (write policy in `where`), writes only
1238
+ * the touched cells into the manual source, then re-checks `insertCheck`
1239
+ * against the resolved row. `revert` clears `revertColumns` from the manual
1240
+ * source; `delete` tombstones the row there.
1241
+ */
1242
+ resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
1243
+ revertColumns: zod.z.array(zod.z.string()).optional(),
1244
+ /** `@writeback(source:, mode:)` directives on this root. */
1245
+ writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
1246
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
1247
+ localOnly: zod.z.array(zod.z.string()).optional(),
593
1248
  alias: zod.z.string(),
594
1249
  table: zod.z.string(),
595
1250
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -653,7 +1308,31 @@ var variableIrSchema = zod.z.object({
653
1308
  /** Absent = no default declared; see {@link variableDefaultIrSchema}. */
654
1309
  defaultValue: variableDefaultIrSchema.optional()
655
1310
  });
1311
+ var sqlColumnSchema = zod.z.object({ name: zod.z.string(), type: zod.z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]), nullable: zod.z.boolean() });
656
1312
  var compiledOperationSchema = zod.z.object({
1313
+ language: zod.z.literal("sql").optional(),
1314
+ /** Server-compiled object catalog and permission ceiling, never caller SQL. */
1315
+ sqlQuery: zod.z.object({
1316
+ catalog: zod.z.object({
1317
+ identity: zod.z.string(),
1318
+ objects: zod.z.array(zod.z.object({
1319
+ ref: zod.z.string(),
1320
+ table: zod.z.string(),
1321
+ columns: zod.z.array(sqlColumnSchema.extend({ type: zod.z.string() })).min(1),
1322
+ scan: zod.z.string().min(1),
1323
+ read: zod.z.array(zod.z.string()).min(1)
1324
+ }).strict()).min(1).max(256)
1325
+ }).strict(),
1326
+ read: zod.z.array(zod.z.string()).min(1).max(256),
1327
+ maxRows: zod.z.number().int().min(1).max(1e4)
1328
+ }).strict().optional(),
1329
+ sql: zod.z.object({
1330
+ text: zod.z.string().min(1).max(1048576),
1331
+ columns: zod.z.array(sqlColumnSchema).min(1).max(256),
1332
+ parameters: zod.z.array(zod.z.string()).max(128),
1333
+ read: zod.z.array(zod.z.string()).min(1).max(256),
1334
+ maxRows: zod.z.number().int().min(1).max(1e4)
1335
+ }).strict().optional(),
657
1336
  irVersion: irVersionSchema,
658
1337
  name: zod.z.string(),
659
1338
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
@@ -665,8 +1344,16 @@ var compiledOperationSchema = zod.z.object({
665
1344
  variables: zod.z.array(variableIrSchema),
666
1345
  /** Every table the op touches — data-token scoping + audit. */
667
1346
  tableSet: zod.z.array(zod.z.string()).min(1),
668
- roots: zod.z.array(rootIrSchema).min(1)
1347
+ roots: zod.z.array(rootIrSchema)
669
1348
  }).superRefine((operation, ctx) => {
1349
+ if (operation.language === "sql") {
1350
+ const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
1351
+ if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !operation.requires?.capability) {
1352
+ ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
1353
+ }
1354
+ } else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
1355
+ ctx.addIssue({ code: "custom", message: "GraphQL operations require roots and cannot carry SQL" });
1356
+ }
670
1357
  for (const [index, root] of operation.roots.entries()) {
671
1358
  if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
672
1359
  if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
@@ -674,13 +1361,55 @@ var compiledOperationSchema = zod.z.object({
674
1361
  }
675
1362
  }
676
1363
  });
1364
+ var resolvedSourceIrSchema = zod.z.object({
1365
+ name: zod.z.string(),
1366
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
1367
+ /** Logical table key of the backing relation. */
1368
+ table: zod.z.string(),
1369
+ columns: zod.z.array(zod.z.string()),
1370
+ key: zod.z.string().optional(),
1371
+ fetch: zod.z.union([
1372
+ zod.z.object({ shape: zod.z.literal("single") }),
1373
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
1374
+ ]).optional(),
1375
+ function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
1376
+ prefetchDefaults: zod.z.object({
1377
+ maxStalenessMs: zod.z.number().int().positive(),
1378
+ timeoutMs: zod.z.number().int().positive(),
1379
+ onError: zod.z.enum(["fallback", "fail"])
1380
+ }).optional(),
1381
+ writeback: zod.z.object({
1382
+ columns: zod.z.array(zod.z.string()),
1383
+ defaultMode: zod.z.enum(["sync", "async"]),
1384
+ timeoutMs: zod.z.number().int().positive(),
1385
+ clearManualOnConfirm: zod.z.boolean()
1386
+ }).optional()
1387
+ });
1388
+ var resolvedMetaIrSchema = zod.z.object({
1389
+ /** Logical pk column of the resolved view (single-column). */
1390
+ pk: zod.z.string(),
1391
+ /** Source name of the manual source. */
1392
+ manual: zod.z.string(),
1393
+ sources: zod.z.array(resolvedSourceIrSchema),
1394
+ deletedBy: zod.z.array(zod.z.string()),
1395
+ /**
1396
+ * Materialized: the view reads `table`, which the data plane refreshes from
1397
+ * the `compute` view (claims-gated) whenever it writes a source row.
1398
+ */
1399
+ materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
1400
+ });
677
1401
  var tableMetaIrSchema = zod.z.object({
678
1402
  table: zod.z.string(),
1403
+ namespace: zod.z.string().optional(),
1404
+ dependencies: zod.z.array(zod.z.string()).optional(),
1405
+ foreignReferences: zod.z.array(zod.z.object({ column: zod.z.string(), target: zod.z.string(), targetColumn: zod.z.string() })).optional(),
1406
+ exportRead: zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string() }), zod.z.object({ role: zod.z.string() })]).optional(),
1407
+ exportWrite: zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string() }), zod.z.object({ role: zod.z.string() })]).optional(),
679
1408
  schema: zod.z.string(),
680
1409
  typename: zod.z.string(),
681
1410
  /**
682
1411
  * True when statements over this relation evaluate a claims-gated synced
683
- * view: synced twins (`source: gold.*`) materialize as views whose baked
1412
+ * view: synced twins (`source: schema.table`) materialize as views whose baked
684
1413
  * policy reads `current_setting('request.jwt.claims')`, and namespace views
685
1414
  * inherit the gate transitively through their declared dependencies. The
686
1415
  * executor must stamp that GUC (transaction-scoped) before any statement
@@ -714,6 +1443,22 @@ var tableMetaIrSchema = zod.z.object({
714
1443
  * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
715
1444
  */
716
1445
  touchOnUpdate: zod.z.array(zod.z.string()).optional(),
1446
+ /**
1447
+ * Per-cell metadata: logical column → the logical names of its cell-part
1448
+ * columns (which are listed in `columns`, but never filterable, orderable or
1449
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
1450
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
1451
+ * the relation tracks no cells.
1452
+ */
1453
+ cells: zod.z.record(zod.z.string(), zod.z.object({
1454
+ at: zod.z.string().optional(),
1455
+ by: zod.z.string().optional(),
1456
+ rev: zod.z.string().optional(),
1457
+ source: zod.z.string().optional(),
1458
+ conflicts: zod.z.string().optional()
1459
+ }).strict()).optional(),
1460
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
1461
+ resolved: resolvedMetaIrSchema.optional(),
717
1462
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
718
1463
  /** Depth/node caps for dynamic filter trees on this table. */
719
1464
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
@@ -723,10 +1468,18 @@ var compiledArtifactSchema = zod.z.object({
723
1468
  irVersion: irVersionSchema,
724
1469
  namespace: zod.z.string(),
725
1470
  schemaHash: zod.z.string(),
1471
+ ownSchemaHash: zod.z.string().optional(),
1472
+ dependencies: zod.z.record(zod.z.string(), zod.z.string()).optional(),
1473
+ readPolicies: zod.z.record(zod.z.string(), predIrSchema).optional(),
726
1474
  opsHash: zod.z.string(),
1475
+ /** Apply-observed synced objects without a serving backing; not part of the authored schema hash. */
1476
+ unbackedTables: zod.z.array(zod.z.string()).optional(),
727
1477
  tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
728
1478
  operations: zod.z.record(zod.z.string(), compiledOperationSchema)
729
1479
  }).superRefine((artifact, ctx) => {
1480
+ if (Object.keys(artifact.dependencies ?? {}).length > 0 && artifact.irVersion < COMPOSED_IR_VERSION) {
1481
+ ctx.addIssue({ code: "custom", path: ["irVersion"], message: "cross-namespace dependencies require IR v3" });
1482
+ }
730
1483
  for (const [name, operation] of Object.entries(artifact.operations)) {
731
1484
  if (operation.irVersion > artifact.irVersion) {
732
1485
  ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
@@ -742,7 +1495,7 @@ var IrVersionError = class extends Error {
742
1495
  function normalizeIr(raw) {
743
1496
  if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
744
1497
  const version = raw.irVersion;
745
- if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
1498
+ if (!irVersionSchema.safeParse(version).success) throw new IrVersionError(version);
746
1499
  return compiledArtifactSchema.parse(raw);
747
1500
  }
748
1501
  function indexByHash(artifact) {
@@ -751,13 +1504,20 @@ function indexByHash(artifact) {
751
1504
  return map;
752
1505
  }
753
1506
 
1507
+ exports.CELLS_FIELD = CELLS_FIELD;
1508
+ exports.CELL_PARTS = CELL_PARTS;
754
1509
  exports.CLAIMS_CONTRACT = CLAIMS_CONTRACT;
755
1510
  exports.CMP_OPS = CMP_OPS;
1511
+ exports.COMPOSED_IR_VERSION = COMPOSED_IR_VERSION;
756
1512
  exports.DATE_BUCKETS = DATE_BUCKETS;
1513
+ exports.GUARDED_IR_VERSION = GUARDED_IR_VERSION;
757
1514
  exports.INPUT_REF_KINDS = INPUT_REF_KINDS;
758
1515
  exports.IR_VERSION = IR_VERSION;
759
1516
  exports.IrVersionError = IrVersionError;
760
1517
  exports.LEGACY_IR_VERSION = LEGACY_IR_VERSION;
1518
+ exports.RUNTIME_SQL_IR_VERSION = RUNTIME_SQL_IR_VERSION;
1519
+ exports.SQL_IR_VERSION = SQL_IR_VERSION;
1520
+ exports.cellColumnName = cellColumnName;
761
1521
  exports.compiledArtifactSchema = compiledArtifactSchema;
762
1522
  exports.compiledOperationSchema = compiledOperationSchema;
763
1523
  exports.indexByHash = indexByHash;
@@ -765,7 +1525,10 @@ exports.limitIrSchema = limitIrSchema;
765
1525
  exports.normalizeIr = normalizeIr;
766
1526
  exports.orderIrSchema = orderIrSchema;
767
1527
  exports.predIrSchema = predIrSchema;
1528
+ exports.prefetchPlanIrSchema = prefetchPlanIrSchema;
768
1529
  exports.requiredArtifactIrVersion = requiredArtifactIrVersion;
1530
+ exports.resolvedMetaIrSchema = resolvedMetaIrSchema;
1531
+ exports.resolvedSourceIrSchema = resolvedSourceIrSchema;
769
1532
  exports.rootIrSchema = rootIrSchema;
770
1533
  exports.selectionIrSchema = selectionIrSchema;
771
1534
  exports.tableMetaIrSchema = tableMetaIrSchema;