@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/sdl.cjs CHANGED
@@ -4,14 +4,480 @@ var zod = require('zod');
4
4
  var crypto = require('crypto');
5
5
 
6
6
  // src/canonical/v2.ts
7
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
8
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
9
+
10
+ // src/checks/compile.ts
11
+ function tokenize(source) {
12
+ const tokens = [];
13
+ for (let position = 0; position < source.length; ) {
14
+ const char = source[position];
15
+ if (/\s/.test(char)) {
16
+ position++;
17
+ continue;
18
+ }
19
+ if (char === "(" || char === ")" || char === ",") {
20
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
21
+ position++;
22
+ continue;
23
+ }
24
+ if (char === "'") {
25
+ const start = position++;
26
+ let value = "";
27
+ let closed = false;
28
+ while (position < source.length) {
29
+ if (source[position] !== "'") {
30
+ value += source[position++];
31
+ continue;
32
+ }
33
+ if (source[position + 1] === "'") {
34
+ value += "'";
35
+ position += 2;
36
+ continue;
37
+ }
38
+ position++;
39
+ closed = true;
40
+ break;
41
+ }
42
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
43
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
44
+ tokens.push({ kind: "string", value, position: start });
45
+ continue;
46
+ }
47
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
48
+ if (operator) {
49
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
50
+ position += operator[0].length;
51
+ continue;
52
+ }
53
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
54
+ if (number) {
55
+ tokens.push({ kind: "number", value: number[0], position });
56
+ position += number[0].length;
57
+ continue;
58
+ }
59
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
60
+ if (identifier3) {
61
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
62
+ position += identifier3[0].length;
63
+ continue;
64
+ }
65
+ throw new Error(`unexpected character '${char}' at position ${position}`);
66
+ }
67
+ tokens.push({ kind: "end", value: "", position: source.length });
68
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
69
+ return tokens;
70
+ }
71
+ var Parser = class {
72
+ constructor(tokens, columns) {
73
+ this.tokens = tokens;
74
+ this.columns = /* @__PURE__ */ new Map();
75
+ for (const column of columns) {
76
+ for (const alias of [column.name, column.columnName]) {
77
+ const previous = this.columns.get(alias);
78
+ if (previous && previous.columnName !== column.columnName) {
79
+ throw new Error(`ambiguous check column '${alias}'`);
80
+ }
81
+ this.columns.set(alias, column);
82
+ }
83
+ }
84
+ }
85
+ index = 0;
86
+ depth = 0;
87
+ columns;
88
+ parse() {
89
+ const expression = this.parseOr();
90
+ this.expect("end", "end of expression");
91
+ return expression;
92
+ }
93
+ parseOr() {
94
+ let left = this.parseAnd();
95
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
96
+ return left;
97
+ }
98
+ parseAnd() {
99
+ let left = this.parseNot();
100
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
101
+ return left;
102
+ }
103
+ parseNot() {
104
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
105
+ return this.parsePrimary();
106
+ }
107
+ parsePrimary() {
108
+ if (this.peek().kind === "lparen") {
109
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
110
+ this.advance();
111
+ const expression = this.parseOr();
112
+ this.expect("rparen", "')'");
113
+ this.depth--;
114
+ return `(${expression})`;
115
+ }
116
+ return this.parseComparison();
117
+ }
118
+ parseComparison() {
119
+ const left = this.parseValue();
120
+ if (this.matchKeyword("is")) {
121
+ const negate = this.matchKeyword("not");
122
+ this.expectKeyword("null");
123
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
124
+ }
125
+ if (this.matchKeyword("between")) {
126
+ const lower = this.parseValue();
127
+ this.expectKeyword("and");
128
+ const upper = this.parseValue();
129
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
130
+ }
131
+ if (this.matchKeyword("in")) {
132
+ this.expect("lparen", "'(' after IN");
133
+ const values = [this.parseValue()];
134
+ while (this.peek().kind === "comma") {
135
+ this.advance();
136
+ values.push(this.parseValue());
137
+ }
138
+ this.expect("rparen", "')' after IN values");
139
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
140
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
141
+ }
142
+ const operator = this.expect("operator", "a comparison operator");
143
+ const right = this.parseValue();
144
+ if (operator.value === "~" || operator.value === "!~") {
145
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
146
+ if (!right.literal) throw new Error("regex pattern must be a literal");
147
+ }
148
+ return `${left.sql} ${operator.value} ${right.sql}`;
149
+ }
150
+ parseValue() {
151
+ const token = this.advance();
152
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
153
+ if (token.kind === "number") {
154
+ const number = Number(token.value);
155
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
156
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
157
+ }
158
+ return { sql: token.value, literal: true };
159
+ }
160
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
161
+ const keyword = token.value.toLowerCase();
162
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
163
+ if (keyword === "btrim") {
164
+ this.expect("lparen", "'(' after btrim");
165
+ const column2 = this.parseColumn();
166
+ this.expect("rparen", "')' after btrim column");
167
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
168
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
169
+ }
170
+ const column = this.columns.get(token.value);
171
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
172
+ return { sql: quoteIdentifier(column.columnName), column };
173
+ }
174
+ parseColumn() {
175
+ const token = this.expect("identifier", "a column name");
176
+ const column = this.columns.get(token.value);
177
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
178
+ return column;
179
+ }
180
+ peek() {
181
+ return this.tokens[this.index];
182
+ }
183
+ advance() {
184
+ const token = this.peek();
185
+ if (token.kind !== "end") this.index++;
186
+ return token;
187
+ }
188
+ matchKeyword(keyword) {
189
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
190
+ this.advance();
191
+ return true;
192
+ }
193
+ expectKeyword(keyword) {
194
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
195
+ }
196
+ expect(kind, description) {
197
+ const token = this.peek();
198
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
199
+ return this.advance();
200
+ }
201
+ };
202
+ function compileCheckExpression(source, columns) {
203
+ return new Parser(tokenize(source), columns).parse();
204
+ }
7
205
 
8
206
  // src/constants.ts
9
207
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
208
+ var MAX_IDENTIFIER_LENGTH = 63;
209
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
210
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
211
+ var CELL_PART_SUFFIX = {
212
+ at: "__at",
213
+ by: "__by",
214
+ rev: "__rev",
215
+ source: "__src",
216
+ conflicts: "__lost"
217
+ };
218
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
219
+ var CELLS_FIELD = "cells";
220
+ function cellColumnName(column, part) {
221
+ return `${column}${CELL_PART_SUFFIX[part]}`;
222
+ }
223
+ var MATERIALIZE_ACTOR = "orm:materialize";
224
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
225
+ var policyVariantSchema = zod.z.object({
226
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
227
+ when: zod.z.string().min(1).max(1e3),
228
+ /** Exactly one of: allow-all bypass, or a row predicate. */
229
+ allow: zod.z.literal(true).optional(),
230
+ policy: zod.z.string().min(1).max(2e3).optional()
231
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
232
+ message: "A variant declares exactly one of allow: true or policy"
233
+ });
234
+ var policyExpressionSchema = zod.z.union([
235
+ zod.z.string().min(1).max(2e3),
236
+ zod.z.object({
237
+ default: zod.z.string().min(1).max(2e3),
238
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
239
+ })
240
+ ]);
241
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
242
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
243
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
244
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
245
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
246
+ };
247
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
248
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
249
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
250
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
251
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
252
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
253
+ };
254
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
255
+ /* @__PURE__ */ new Set([
256
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
257
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
258
+ ]);
259
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
260
+ var canonicalFunctionRefSchema = zod.z.object({
261
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
262
+ kind: zod.z.enum(["managed", "native"]),
263
+ id: zod.z.string().min(1).max(200)
264
+ }).strict();
265
+ var canonicalFetchShapeSchema = zod.z.union([
266
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
267
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
268
+ ]);
269
+ var canonicalResolvedSourceSchema = zod.z.object({
270
+ name: identifier,
271
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
272
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
273
+ table: identifier,
274
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
275
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
276
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
277
+ updatedAt: zod.z.string().min(1).max(63).optional(),
278
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
279
+ key: zod.z.string().min(1).max(63).optional(),
280
+ fetch: canonicalFetchShapeSchema.optional(),
281
+ function: canonicalFunctionRefSchema.optional(),
282
+ prefetchDefaults: zod.z.object({
283
+ maxStalenessMs: durationMs,
284
+ timeoutMs: durationMs,
285
+ onError: zod.z.enum(["fallback", "fail"])
286
+ }).strict().optional(),
287
+ writeback: zod.z.object({
288
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
289
+ defaultMode: zod.z.enum(["sync", "async"]),
290
+ timeoutMs: durationMs,
291
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
292
+ clearManualOnConfirm: zod.z.boolean()
293
+ }).strict().optional()
294
+ }).strict();
295
+ var canonicalResolveRuleSchema = zod.z.object({
296
+ column: zod.z.string().min(1).max(63),
297
+ comparator: zod.z.enum(COMPARATORS),
298
+ /** Source names, in tie-break order. */
299
+ candidates: zod.z.array(identifier).min(1).max(16),
300
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
301
+ orderBy: zod.z.string().min(1).max(2e3).optional()
302
+ }).strict();
303
+ var canonicalResolvedSchema = zod.z.object({
304
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
305
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
306
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
307
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
308
+ deletedBy: zod.z.array(identifier).max(1).default([]),
309
+ /**
310
+ * Materialized: the view reads an indexed internal table (`table`) that the
311
+ * data plane keeps in step with `compute`, the internal view holding the
312
+ * resolution query. Omitted when the view computes on read.
313
+ */
314
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
315
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
316
+ policies: zod.z.object({
317
+ write: policyExpressionSchema.optional(),
318
+ insert: policyExpressionSchema.optional(),
319
+ update: policyExpressionSchema.optional()
320
+ }).strict().default({})
321
+ }).strict();
322
+ function validateResolvedView({ view, tables, views = [] }) {
323
+ const resolved = view.resolved;
324
+ if (!resolved) return [];
325
+ const errors = [];
326
+ const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
327
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
328
+ if (!pkColumn) {
329
+ fail("requires exactly one primary-key column");
330
+ return errors;
331
+ }
332
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
333
+ const scope = {
334
+ view,
335
+ resolved,
336
+ pkColumn,
337
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
338
+ sourceByName: /* @__PURE__ */ new Map(),
339
+ fail
340
+ };
341
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
342
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
343
+ for (const source of resolved.sources) {
344
+ if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
345
+ scope.sourceByName.set(source.name, source);
346
+ const table = tableByName.get(source.table);
347
+ if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
348
+ else validateSource({ scope, source, table });
349
+ }
350
+ validateRules(scope);
351
+ validateViewCells(scope);
352
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
353
+ for (const name of resolved.deletedBy) {
354
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
355
+ }
356
+ return errors;
357
+ }
358
+ function validateSource({ scope, source, table }) {
359
+ const { fail } = scope;
360
+ if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
361
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
362
+ fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
363
+ }
364
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
365
+ if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
366
+ if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
367
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
368
+ validateSourceColumns({ scope, source, byName });
369
+ if (source.kind === "etl") {
370
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
371
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
372
+ fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
373
+ }
374
+ }
375
+ validatePrefetchFields({ scope, source });
376
+ }
377
+ function validatePrefetchFields({ scope, source }) {
378
+ const { fail, pkColumn } = scope;
379
+ if (source.kind !== "prefetch") {
380
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
381
+ fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
382
+ }
383
+ return;
384
+ }
385
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
386
+ fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
387
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
388
+ fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
389
+ }
390
+ for (const column of source.writeback?.columns ?? []) {
391
+ if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
392
+ }
393
+ }
394
+ function validateSourceColumns({ scope, source, byName }) {
395
+ const { fail, pkColumn, dataColumns } = scope;
396
+ for (const column of [pkColumn.name, ...source.columns]) {
397
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
398
+ const theirs = byName.get(column);
399
+ if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
400
+ else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
401
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
402
+ }
403
+ }
404
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
405
+ const ruled = /* @__PURE__ */ new Set();
406
+ for (const rule of resolved.resolve) {
407
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
408
+ fail(`resolve rule targets unknown column '${rule.column}'`);
409
+ continue;
410
+ }
411
+ if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
412
+ ruled.add(rule.column);
413
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
414
+ fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
415
+ }
416
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
417
+ for (const candidate of rule.candidates) {
418
+ const source = sourceByName.get(candidate);
419
+ if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
420
+ else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
421
+ }
422
+ }
423
+ for (const column of dataColumns.keys()) {
424
+ if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
425
+ }
426
+ }
427
+ var VIEW_CELL_TYPES = {
428
+ source: "text",
429
+ at: "timestamptz",
430
+ by: "text",
431
+ conflicts: "jsonb"
432
+ };
433
+ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
434
+ const expected = /* @__PURE__ */ new Map();
435
+ for (const column of dataColumns.values()) {
436
+ if (column.name === pkColumn.name) continue;
437
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
438
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
439
+ }
440
+ }
441
+ for (const column of view.columns) {
442
+ if (!column.cell) continue;
443
+ const want = expected.get(column.name);
444
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
445
+ fail(`unexpected cell column '${column.name}'`);
446
+ continue;
447
+ }
448
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
449
+ expected.delete(column.name);
450
+ }
451
+ for (const name of expected.keys()) fail(`missing cell column '${name}'`);
452
+ }
453
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
454
+ function validateMaterialized({ scope, tableByName, views }) {
455
+ const { view, resolved, fail } = scope;
456
+ const { table: tableName, compute: computeName } = resolved.materialized;
457
+ const table = tableByName.get(tableName);
458
+ if (!table || table.internal !== true || table.materializes !== view.view) {
459
+ fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
460
+ } else {
461
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
462
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
463
+ }
464
+ const compute = views.find((candidate) => candidate.view === computeName);
465
+ if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
466
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
467
+ for (const source of resolved.sources) {
468
+ if (source.kind !== "etl") continue;
469
+ const read = tableByName.get(source.table)?.policies?.read;
470
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
471
+ if (!unconditional) {
472
+ fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
473
+ }
474
+ }
475
+ }
10
476
 
11
477
  // src/canonical/v2.ts
12
478
  var CANONICAL_FORMAT = 2;
13
479
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
14
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
480
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
15
481
  var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
16
482
  var COLUMN_TYPES = [
17
483
  "uuid",
@@ -27,14 +493,14 @@ var COLUMN_TYPES = [
27
493
  ];
28
494
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
29
495
  var canonicalEnumSchema = zod.z.object({
30
- name: identifier,
496
+ name: identifier2,
31
497
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
32
498
  });
33
499
  var canonicalColumnSchema = zod.z.object({
34
500
  /** Property name in TS (camelCase); columnName is the Postgres name. */
35
501
  name: zod.z.string().min(1).max(63),
36
502
  columnName: columnIdentifier,
37
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
503
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
38
504
  nullable: zod.z.boolean().default(true),
39
505
  unique: zod.z.boolean().default(false),
40
506
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -50,7 +516,14 @@ var canonicalColumnSchema = zod.z.object({
50
516
  /** Migration hints — same semantics as v1. */
51
517
  renamedFrom: columnIdentifier.optional(),
52
518
  cast: zod.z.string().max(200).optional(),
53
- deprecated: zod.z.boolean().default(false)
519
+ deprecated: zod.z.boolean().default(false),
520
+ /**
521
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
522
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
523
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
524
+ * ordinary columns so their hashes are unchanged.
525
+ */
526
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
54
527
  }).superRefine((column, ctx) => {
55
528
  if (column.onUpdateFn === void 0) return;
56
529
  if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
@@ -65,36 +538,28 @@ var canonicalLinkSchema = zod.z.object({
65
538
  name: zod.z.string().min(1).max(63),
66
539
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
67
540
  /** Target table name (namespace-local). Absent for toPlatformUser. */
68
- target: identifier.optional(),
541
+ target: identifier2.optional(),
542
+ /** Owning namespace of an imported target; omitted for local links. */
543
+ namespace: identifier2.max(41).optional(),
69
544
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
70
545
  on: columnIdentifier,
71
546
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
72
547
  references: columnIdentifier.optional(),
73
548
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
74
549
  enforce: zod.z.boolean().default(false),
550
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
551
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
75
552
  description: zod.z.string().max(500).optional()
76
553
  });
77
554
  var canonicalIndexSchema = zod.z.object({
78
555
  columns: zod.z.array(columnIdentifier).min(1),
79
556
  unique: zod.z.boolean().default(false),
80
- name: identifier.optional()
81
- });
82
- var policyVariantSchema = zod.z.object({
83
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
84
- when: zod.z.string().min(1).max(1e3),
85
- /** Exactly one of: allow-all bypass, or a row predicate. */
86
- allow: zod.z.literal(true).optional(),
87
- policy: zod.z.string().min(1).max(2e3).optional()
88
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
89
- message: "A variant declares exactly one of allow: true or policy"
557
+ name: identifier2.optional()
90
558
  });
91
- var policyExpressionSchema = zod.z.union([
92
- zod.z.string().min(1).max(2e3),
93
- zod.z.object({
94
- default: zod.z.string().min(1).max(2e3),
95
- variants: zod.z.array(policyVariantSchema).max(10).default([])
96
- })
97
- ]);
559
+ var canonicalCheckSchema = zod.z.object({
560
+ name: identifier2,
561
+ expression: zod.z.string().min(1).max(2e3)
562
+ }).strict();
98
563
  var canonicalPoliciesSchema = zod.z.object({
99
564
  read: policyExpressionSchema.optional(),
100
565
  write: policyExpressionSchema.optional(),
@@ -125,7 +590,7 @@ function isSingleViewQuerySql(value) {
125
590
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
126
591
  }
127
592
  var canonicalActionSchema = zod.z.object({
128
- name: identifier,
593
+ name: identifier2,
129
594
  requires: zod.z.string().max(1e3).optional(),
130
595
  params: zod.z.record(zod.z.string(), zod.z.object({
131
596
  type: columnTypeSchema,
@@ -137,9 +602,9 @@ var canonicalActionSchema = zod.z.object({
137
602
  });
138
603
  var canonicalCowMutationSchema = zod.z.object({
139
604
  kind: zod.z.enum(["override", "revert"]),
140
- name: identifier,
605
+ name: identifier2,
141
606
  /** Synced table (same namespace) the overlay copies from / falls back to. */
142
- synced: identifier,
607
+ synced: identifier2,
143
608
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
144
609
  pk: zod.z.string().min(1).max(63),
145
610
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -167,23 +632,28 @@ var canonicalCowMutationSchema = zod.z.object({
167
632
  }
168
633
  });
169
634
  var canonicalForeignKeySchema = zod.z.object({
170
- name: identifier,
635
+ name: identifier2,
171
636
  columns: zod.z.array(columnIdentifier).min(1).max(8),
172
- target: identifier,
637
+ target: identifier2,
173
638
  references: zod.z.array(columnIdentifier).min(1).max(8),
174
- deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
639
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
640
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
641
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
175
642
  }).strict();
176
643
  var canonicalTableSchema = zod.z.object({
177
- table: identifier,
644
+ table: identifier2,
645
+ displayName: zod.z.string().trim().min(1).max(120).optional(),
178
646
  description: zod.z.string().max(2e3).optional(),
179
647
  /** Table-level rename hint (new in v2). */
180
- renamedFrom: identifier.optional(),
181
- /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
182
- source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
648
+ renamedFrom: identifier2.optional(),
649
+ /** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
650
+ source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
183
651
  columns: zod.z.array(canonicalColumnSchema).default([]),
184
652
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
185
653
  links: zod.z.array(canonicalLinkSchema).default([]),
186
654
  indexes: zod.z.array(canonicalIndexSchema).default([]),
655
+ /** Omitted by old definitions, preserving their existing content hashes. */
656
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
187
657
  policies: canonicalPoliciesSchema.default({}),
188
658
  /**
189
659
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -194,22 +664,55 @@ var canonicalTableSchema = zod.z.object({
194
664
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
195
665
  cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
196
666
  /** Omitted by old definitions, preserving their existing content hashes. */
197
- foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
667
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
668
+ /**
669
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
670
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
671
+ * on a resolved table's internal sources), listed explicitly in `columns`
672
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
673
+ * Omitted when off so existing definitions hash unchanged.
674
+ */
675
+ cellMetadata: zod.z.literal(true).optional(),
676
+ /**
677
+ * System-owned backing table of a resolved table (its manual or prefetch
678
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
679
+ * only the data plane writes them. Omitted when false.
680
+ */
681
+ internal: zod.z.literal(true).optional(),
682
+ /**
683
+ * The materialized copy of resolved table `materializes` (internal): the
684
+ * same columns as that view, including its cell parts, maintained by the
685
+ * data plane. Omitted otherwise.
686
+ */
687
+ materializes: identifier2.optional()
198
688
  });
689
+ var MAX_VIEW_QUERY = 1e5;
690
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
199
691
  var canonicalViewSchema = zod.z.object({
200
- view: identifier,
692
+ view: identifier2,
201
693
  description: zod.z.string().max(2e3).optional(),
202
694
  columns: zod.z.array(canonicalColumnSchema).min(1),
203
695
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
204
696
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
205
697
  links: zod.z.array(canonicalLinkSchema).default([]),
206
698
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
207
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
699
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
208
700
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
209
701
  }),
210
702
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
211
- dependencies: zod.z.array(identifier).max(500)
703
+ dependencies: zod.z.array(identifier2).max(500),
704
+ /**
705
+ * Present when this view is a resolved table (defineResolvedTable): the
706
+ * multi-source metadata behind its generated query. Omitted otherwise so
707
+ * existing views hash unchanged.
708
+ */
709
+ resolved: canonicalResolvedSchema.optional(),
710
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
711
+ internal: zod.z.literal(true).optional()
212
712
  }).superRefine((view, ctx) => {
713
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
714
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
715
+ }
213
716
  for (const [index, column] of view.columns.entries()) {
214
717
  if (column.default !== void 0 || column.defaultFn !== void 0) {
215
718
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
@@ -224,20 +727,91 @@ var canonicalViewSchema = zod.z.object({
224
727
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
225
728
  }
226
729
  }
730
+ if (view.resolved === void 0 && view.internal !== true) {
731
+ for (const [index, column] of view.columns.entries()) {
732
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
733
+ }
734
+ }
227
735
  for (const [index, link] of view.links.entries()) {
228
736
  if (link.enforce) {
229
737
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
230
738
  }
739
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
740
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
741
+ }
231
742
  }
232
743
  });
233
- var canonicalDefinitionSchema = zod.z.object({
744
+ var CELL_PART_TYPES = {
745
+ at: "timestamptz",
746
+ by: "text",
747
+ rev: "uuid",
748
+ source: "text",
749
+ conflicts: "jsonb"
750
+ };
751
+ function trackedCellColumns(table) {
752
+ const pk = new Set(table.primaryKey ?? []);
753
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
754
+ }
755
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
756
+ function validateCellMetadata(table, index, ctx) {
757
+ const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
758
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
759
+ if (table.materializes !== void 0) return;
760
+ if (table.cellMetadata !== true) {
761
+ for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
762
+ return;
763
+ }
764
+ if (table.source !== void 0) {
765
+ fail(`synced table '${table.table}' cannot track cell metadata`);
766
+ return;
767
+ }
768
+ const tracked = trackedCellColumns(table);
769
+ const byName = new Map(tracked.map((column) => [column.name, column]));
770
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
771
+ if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
772
+ const seen = /* @__PURE__ */ new Set();
773
+ for (const { column, position, cell } of cells) {
774
+ const path = ["tables", index, "columns", position];
775
+ const owner = byName.get(cell.of);
776
+ if (!owner) {
777
+ fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
778
+ continue;
779
+ }
780
+ 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);
781
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
782
+ fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
783
+ }
784
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
785
+ fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
786
+ }
787
+ const key = `${cell.of}:${cell.part}`;
788
+ if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
789
+ seen.add(key);
790
+ }
791
+ for (const column of tracked) {
792
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
793
+ fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
794
+ }
795
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
796
+ fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
797
+ }
798
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
799
+ if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
800
+ }
801
+ }
802
+ }
803
+ 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()]);
804
+ var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
805
+ var localDefinitionSchema = zod.z.object({
234
806
  definition_format: zod.z.literal(CANONICAL_FORMAT),
235
- namespace: identifier.max(41),
807
+ namespace: identifier2.max(41),
236
808
  description: zod.z.string().max(2e3).optional(),
237
809
  enums: zod.z.array(canonicalEnumSchema).default([]),
238
810
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
239
811
  /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
240
812
  views: zod.z.array(canonicalViewSchema).max(500).optional(),
813
+ /** Target-owned cross-namespace access contract, using existing namespace grants. */
814
+ exports: zod.z.record(identifier2, relationExportSchema).optional(),
241
815
  /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
242
816
  capabilities: zod.z.object({
243
817
  roles: zod.z.unknown().optional(),
@@ -250,6 +824,7 @@ var canonicalDefinitionSchema = zod.z.object({
250
824
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
251
825
  }
252
826
  seen.add(table.table);
827
+ validateCellMetadata(table, index, ctx);
253
828
  if (table.source !== void 0) {
254
829
  for (const [columnIndex, column] of table.columns.entries()) {
255
830
  if (column.onUpdateFn !== void 0) {
@@ -263,12 +838,41 @@ var canonicalDefinitionSchema = zod.z.object({
263
838
  }
264
839
  }
265
840
  const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
841
+ const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
842
+ for (const name of Object.keys(definition.exports ?? {})) {
843
+ if (!relations.has(name)) ctx.addIssue({ code: "custom", path: ["exports", name], message: `export '${name}' is not a local object` });
844
+ }
266
845
  for (const [tableIndex, table] of definition.tables.entries()) {
846
+ for (const [linkIndex, link] of table.links.entries()) {
847
+ if (link.enforce && link.namespace !== void 0 && link.namespace !== definition.namespace) {
848
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", linkIndex], message: "physical foreign keys require a local target; use an ORM link without enforce" });
849
+ }
850
+ }
267
851
  const names = /* @__PURE__ */ new Set([
268
852
  ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
269
853
  ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
270
854
  ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
271
855
  ]);
856
+ for (const [index, link] of table.links.entries()) {
857
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
858
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
859
+ }
860
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
861
+ const source = table.columns.find((column) => column.columnName === link.on);
862
+ 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" });
863
+ }
864
+ }
865
+ for (const [index, check] of (table.checks ?? []).entries()) {
866
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
867
+ if (table.source) fail("synced tables cannot declare checks");
868
+ if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
869
+ names.add(check.name);
870
+ try {
871
+ compileCheckExpression(check.expression, table.columns);
872
+ } catch (error) {
873
+ fail(error instanceof Error ? error.message : String(error));
874
+ }
875
+ }
272
876
  for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
273
877
  const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
274
878
  if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
@@ -286,10 +890,18 @@ var canonicalDefinitionSchema = zod.z.object({
286
890
  if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
287
891
  else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
288
892
  }
893
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
894
+ 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");
895
+ }
289
896
  const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
290
897
  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");
291
898
  }
292
899
  }
900
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
901
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
902
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
903
+ }
904
+ }
293
905
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
294
906
  for (const [tableIndex, table] of definition.tables.entries()) {
295
907
  const cowMutations = table.cowMutations ?? [];
@@ -405,6 +1017,10 @@ var canonicalDefinitionSchema = zod.z.object({
405
1017
  };
406
1018
  for (const view of views) visit(view.view, []);
407
1019
  });
1020
+ var canonicalDefinitionSchema = localDefinitionSchema.safeExtend({
1021
+ /** Flattened, pinned snapshots. Imported relations never participate in this namespace's DDL. */
1022
+ imports: zod.z.array(localDefinitionSchema).max(32).optional()
1023
+ });
408
1024
  function canonicalRelations(definition) {
409
1025
  const views = (definition.views ?? []).map((view) => ({
410
1026
  table: view.view,
@@ -413,8 +1029,12 @@ function canonicalRelations(definition) {
413
1029
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
414
1030
  links: view.links,
415
1031
  indexes: [],
416
- policies: { read: view.policies.read },
417
- actions: []
1032
+ // A resolved table's write policies (evaluated against the resolved row)
1033
+ // ride alongside the view's read policy.
1034
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
1035
+ actions: [],
1036
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
1037
+ ...view.internal ? { internal: true } : {}
418
1038
  }));
419
1039
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
420
1040
  }
@@ -424,13 +1044,14 @@ function normalizeCanonical(definition) {
424
1044
  ...definition,
425
1045
  enums: sortByName(definition.enums),
426
1046
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
427
- const { cowMutations, foreignKeys, ...rest } = t;
1047
+ const { cowMutations, foreignKeys, checks, ...rest } = t;
428
1048
  return {
429
1049
  ...rest,
430
1050
  links: sortByName(t.links),
431
1051
  indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
432
1052
  actions: sortByName(t.actions),
433
1053
  ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1054
+ ...checks?.length ? { checks: sortByName(checks) } : {},
434
1055
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
435
1056
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
436
1057
  };
@@ -443,6 +1064,11 @@ function normalizeCanonical(definition) {
443
1064
  dependencies: [...view.dependencies].sort()
444
1065
  }));
445
1066
  }
1067
+ if (definition.imports?.length) {
1068
+ normalized.imports = definition.imports.map(normalizeCanonical).sort((a, b) => a.namespace.localeCompare(b.namespace));
1069
+ } else {
1070
+ delete normalized.imports;
1071
+ }
446
1072
  return normalized;
447
1073
  }
448
1074
 
@@ -536,6 +1162,7 @@ function tableNames(table) {
536
1162
  updateByPkRoot: `update${rootPascal}ByPk`,
537
1163
  deleteRoot: `delete${rootPascal}`,
538
1164
  deleteByPkRoot: `delete${rootPascal}ByPk`,
1165
+ revertByPkRoot: `revert${rootPascal}ByPk`,
539
1166
  boolExp: `${type}BoolExp`,
540
1167
  orderBy: `${type}OrderBy`,
541
1168
  selectColumn: `${type}SelectColumn`,
@@ -561,9 +1188,23 @@ function tableNames(table) {
561
1188
  aggregateGroup: `${type}AggregateGroup`
562
1189
  };
563
1190
  }
564
- var IR_VERSION = 2;
1191
+ var CELL_TYPENAMES = {
1192
+ /** A plain table's cell: when and by whom it was last written. */
1193
+ stamp: "CellStamp",
1194
+ /** A resolved table's cell: which source won, when, by whom, and newer losers. */
1195
+ resolved: "ResolvedCell",
1196
+ /** A newer candidate that lost to the column's rule. */
1197
+ conflict: "CellConflict"
1198
+ };
1199
+ function cellsTypename(typename) {
1200
+ return `${typename}Cells`;
1201
+ }
565
1202
  var LEGACY_IR_VERSION = 1;
566
- var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
1203
+ var GUARDED_IR_VERSION = 2;
1204
+ var COMPOSED_IR_VERSION = 3;
1205
+ var SQL_IR_VERSION = 4;
1206
+ var RUNTIME_SQL_IR_VERSION = 5;
1207
+ 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)]);
567
1208
  zod.z.object({ irVersion: irVersionSchema });
568
1209
  var scalarLiteral = zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]);
569
1210
  var valueIrSchema = zod.z.discriminatedUnion("kind", [
@@ -601,8 +1242,8 @@ var predIrSchema = zod.z.lazy(
601
1242
  value: zod.z.number(),
602
1243
  where: predIrSchema.optional()
603
1244
  }),
604
- zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string() }),
605
- zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string() }),
1245
+ zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string(), namespace: zod.z.string().optional() }),
1246
+ zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string(), namespace: zod.z.string().optional() }),
606
1247
  zod.z.object({ kind: zod.z.literal("dynamicFilter"), slot: zod.z.string() }),
607
1248
  zod.z.object({ kind: zod.z.literal("const"), value: zod.z.boolean() })
608
1249
  ])
@@ -630,6 +1271,21 @@ var selectionIrSchema = zod.z.lazy(
630
1271
  () => zod.z.object({
631
1272
  typename: zod.z.string(),
632
1273
  columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
1274
+ cells: zod.z.object({
1275
+ alias: zod.z.string(),
1276
+ typename: zod.z.string(),
1277
+ fields: zod.z.array(zod.z.object({
1278
+ alias: zod.z.string(),
1279
+ column: zod.z.string(),
1280
+ typename: zod.z.string(),
1281
+ parts: zod.z.array(zod.z.object({
1282
+ alias: zod.z.string(),
1283
+ part: zod.z.enum(["source", "at", "by", "conflicts"]),
1284
+ fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
1285
+ typename: zod.z.string().optional()
1286
+ }))
1287
+ }))
1288
+ }).optional(),
633
1289
  relations: zod.z.array(zod.z.object({
634
1290
  alias: zod.z.string(),
635
1291
  link: zod.z.string(),
@@ -651,6 +1307,11 @@ var selectionIrSchema = zod.z.lazy(
651
1307
  })
652
1308
  );
653
1309
  var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
1310
+ var prefetchPlanIrSchema = zod.z.object({
1311
+ source: zod.z.string(),
1312
+ maxStalenessMs: zod.z.number().int().positive().optional(),
1313
+ key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
1314
+ });
654
1315
  var selectRootSchema = zod.z.object({
655
1316
  kind: zod.z.literal("select"),
656
1317
  alias: zod.z.string(),
@@ -662,7 +1323,8 @@ var selectRootSchema = zod.z.object({
662
1323
  distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
663
1324
  limit: limitIrSchema,
664
1325
  offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
665
- includeIf: includeIfSchema.optional()
1326
+ includeIf: includeIfSchema.optional(),
1327
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
666
1328
  });
667
1329
  var byPkRootSchema = zod.z.object({
668
1330
  kind: zod.z.literal("byPk"),
@@ -672,7 +1334,8 @@ var byPkRootSchema = zod.z.object({
672
1334
  pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
673
1335
  /** Policy predicate — ANDed with the PK match. */
674
1336
  where: predIrSchema,
675
- includeIf: includeIfSchema.optional()
1337
+ includeIf: includeIfSchema.optional(),
1338
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
676
1339
  });
677
1340
  var aggregateRootSchema = zod.z.object({
678
1341
  kind: zod.z.literal("aggregate"),
@@ -730,7 +1393,20 @@ var mutationRootSchema = zod.z.object({
730
1393
  * coalesce view falls back to synced. Both are gated on a matching synced
731
1394
  * row and carry the write policy in `where` like update/delete.
732
1395
  */
733
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
1396
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
1397
+ /**
1398
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
1399
+ * executor gates on the resolved row (write policy in `where`), writes only
1400
+ * the touched cells into the manual source, then re-checks `insertCheck`
1401
+ * against the resolved row. `revert` clears `revertColumns` from the manual
1402
+ * source; `delete` tombstones the row there.
1403
+ */
1404
+ resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
1405
+ revertColumns: zod.z.array(zod.z.string()).optional(),
1406
+ /** `@writeback(source:, mode:)` directives on this root. */
1407
+ writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
1408
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
1409
+ localOnly: zod.z.array(zod.z.string()).optional(),
734
1410
  alias: zod.z.string(),
735
1411
  table: zod.z.string(),
736
1412
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -794,7 +1470,31 @@ var variableIrSchema = zod.z.object({
794
1470
  /** Absent = no default declared; see {@link variableDefaultIrSchema}. */
795
1471
  defaultValue: variableDefaultIrSchema.optional()
796
1472
  });
1473
+ 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() });
797
1474
  var compiledOperationSchema = zod.z.object({
1475
+ language: zod.z.literal("sql").optional(),
1476
+ /** Server-compiled object catalog and permission ceiling, never caller SQL. */
1477
+ sqlQuery: zod.z.object({
1478
+ catalog: zod.z.object({
1479
+ identity: zod.z.string(),
1480
+ objects: zod.z.array(zod.z.object({
1481
+ ref: zod.z.string(),
1482
+ table: zod.z.string(),
1483
+ columns: zod.z.array(sqlColumnSchema.extend({ type: zod.z.string() })).min(1),
1484
+ scan: zod.z.string().min(1),
1485
+ read: zod.z.array(zod.z.string()).min(1)
1486
+ }).strict()).min(1).max(256)
1487
+ }).strict(),
1488
+ read: zod.z.array(zod.z.string()).min(1).max(256),
1489
+ maxRows: zod.z.number().int().min(1).max(1e4)
1490
+ }).strict().optional(),
1491
+ sql: zod.z.object({
1492
+ text: zod.z.string().min(1).max(1048576),
1493
+ columns: zod.z.array(sqlColumnSchema).min(1).max(256),
1494
+ parameters: zod.z.array(zod.z.string()).max(128),
1495
+ read: zod.z.array(zod.z.string()).min(1).max(256),
1496
+ maxRows: zod.z.number().int().min(1).max(1e4)
1497
+ }).strict().optional(),
798
1498
  irVersion: irVersionSchema,
799
1499
  name: zod.z.string(),
800
1500
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
@@ -806,8 +1506,16 @@ var compiledOperationSchema = zod.z.object({
806
1506
  variables: zod.z.array(variableIrSchema),
807
1507
  /** Every table the op touches — data-token scoping + audit. */
808
1508
  tableSet: zod.z.array(zod.z.string()).min(1),
809
- roots: zod.z.array(rootIrSchema).min(1)
1509
+ roots: zod.z.array(rootIrSchema)
810
1510
  }).superRefine((operation, ctx) => {
1511
+ if (operation.language === "sql") {
1512
+ const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
1513
+ if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !operation.requires?.capability) {
1514
+ ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
1515
+ }
1516
+ } else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
1517
+ ctx.addIssue({ code: "custom", message: "GraphQL operations require roots and cannot carry SQL" });
1518
+ }
811
1519
  for (const [index, root] of operation.roots.entries()) {
812
1520
  if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
813
1521
  if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
@@ -815,13 +1523,55 @@ var compiledOperationSchema = zod.z.object({
815
1523
  }
816
1524
  }
817
1525
  });
1526
+ var resolvedSourceIrSchema = zod.z.object({
1527
+ name: zod.z.string(),
1528
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
1529
+ /** Logical table key of the backing relation. */
1530
+ table: zod.z.string(),
1531
+ columns: zod.z.array(zod.z.string()),
1532
+ key: zod.z.string().optional(),
1533
+ fetch: zod.z.union([
1534
+ zod.z.object({ shape: zod.z.literal("single") }),
1535
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
1536
+ ]).optional(),
1537
+ function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
1538
+ prefetchDefaults: zod.z.object({
1539
+ maxStalenessMs: zod.z.number().int().positive(),
1540
+ timeoutMs: zod.z.number().int().positive(),
1541
+ onError: zod.z.enum(["fallback", "fail"])
1542
+ }).optional(),
1543
+ writeback: zod.z.object({
1544
+ columns: zod.z.array(zod.z.string()),
1545
+ defaultMode: zod.z.enum(["sync", "async"]),
1546
+ timeoutMs: zod.z.number().int().positive(),
1547
+ clearManualOnConfirm: zod.z.boolean()
1548
+ }).optional()
1549
+ });
1550
+ var resolvedMetaIrSchema = zod.z.object({
1551
+ /** Logical pk column of the resolved view (single-column). */
1552
+ pk: zod.z.string(),
1553
+ /** Source name of the manual source. */
1554
+ manual: zod.z.string(),
1555
+ sources: zod.z.array(resolvedSourceIrSchema),
1556
+ deletedBy: zod.z.array(zod.z.string()),
1557
+ /**
1558
+ * Materialized: the view reads `table`, which the data plane refreshes from
1559
+ * the `compute` view (claims-gated) whenever it writes a source row.
1560
+ */
1561
+ materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
1562
+ });
818
1563
  var tableMetaIrSchema = zod.z.object({
819
1564
  table: zod.z.string(),
1565
+ namespace: zod.z.string().optional(),
1566
+ dependencies: zod.z.array(zod.z.string()).optional(),
1567
+ foreignReferences: zod.z.array(zod.z.object({ column: zod.z.string(), target: zod.z.string(), targetColumn: zod.z.string() })).optional(),
1568
+ exportRead: zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string() }), zod.z.object({ role: zod.z.string() })]).optional(),
1569
+ exportWrite: zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string() }), zod.z.object({ role: zod.z.string() })]).optional(),
820
1570
  schema: zod.z.string(),
821
1571
  typename: zod.z.string(),
822
1572
  /**
823
1573
  * True when statements over this relation evaluate a claims-gated synced
824
- * view: synced twins (`source: gold.*`) materialize as views whose baked
1574
+ * view: synced twins (`source: schema.table`) materialize as views whose baked
825
1575
  * policy reads `current_setting('request.jwt.claims')`, and namespace views
826
1576
  * inherit the gate transitively through their declared dependencies. The
827
1577
  * executor must stamp that GUC (transaction-scoped) before any statement
@@ -855,6 +1605,22 @@ var tableMetaIrSchema = zod.z.object({
855
1605
  * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
856
1606
  */
857
1607
  touchOnUpdate: zod.z.array(zod.z.string()).optional(),
1608
+ /**
1609
+ * Per-cell metadata: logical column → the logical names of its cell-part
1610
+ * columns (which are listed in `columns`, but never filterable, orderable or
1611
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
1612
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
1613
+ * the relation tracks no cells.
1614
+ */
1615
+ cells: zod.z.record(zod.z.string(), zod.z.object({
1616
+ at: zod.z.string().optional(),
1617
+ by: zod.z.string().optional(),
1618
+ rev: zod.z.string().optional(),
1619
+ source: zod.z.string().optional(),
1620
+ conflicts: zod.z.string().optional()
1621
+ }).strict()).optional(),
1622
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
1623
+ resolved: resolvedMetaIrSchema.optional(),
858
1624
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
859
1625
  /** Depth/node caps for dynamic filter trees on this table. */
860
1626
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
@@ -864,10 +1630,18 @@ zod.z.object({
864
1630
  irVersion: irVersionSchema,
865
1631
  namespace: zod.z.string(),
866
1632
  schemaHash: zod.z.string(),
1633
+ ownSchemaHash: zod.z.string().optional(),
1634
+ dependencies: zod.z.record(zod.z.string(), zod.z.string()).optional(),
1635
+ readPolicies: zod.z.record(zod.z.string(), predIrSchema).optional(),
867
1636
  opsHash: zod.z.string(),
1637
+ /** Apply-observed synced objects without a serving backing; not part of the authored schema hash. */
1638
+ unbackedTables: zod.z.array(zod.z.string()).optional(),
868
1639
  tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
869
1640
  operations: zod.z.record(zod.z.string(), compiledOperationSchema)
870
1641
  }).superRefine((artifact, ctx) => {
1642
+ if (Object.keys(artifact.dependencies ?? {}).length > 0 && artifact.irVersion < COMPOSED_IR_VERSION) {
1643
+ ctx.addIssue({ code: "custom", path: ["irVersion"], message: "cross-namespace dependencies require IR v3" });
1644
+ }
871
1645
  for (const [name, operation] of Object.entries(artifact.operations)) {
872
1646
  if (operation.irVersion > artifact.irVersion) {
873
1647
  ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
@@ -875,6 +1649,70 @@ zod.z.object({
875
1649
  }
876
1650
  });
877
1651
 
1652
+ // src/errors.ts
1653
+ var OrmDefinitionError = class extends Error {
1654
+ constructor(message, file) {
1655
+ super(file ? `${file}: ${message}` : message);
1656
+ this.file = file;
1657
+ }
1658
+ name = "OrmDefinitionError";
1659
+ };
1660
+ function stableJson(value) {
1661
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1662
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1663
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
1664
+ return `{${entries.join(",")}}`;
1665
+ }
1666
+
1667
+ // src/canonical/composition.ts
1668
+ function localContract(definition) {
1669
+ return normalizeCanonical(localDefinitionSchema.parse(definition));
1670
+ }
1671
+ function composeNamespace(definition) {
1672
+ const all = [localContract(definition), ...definition.imports ?? []];
1673
+ const owners = /* @__PURE__ */ new Map();
1674
+ const namespaces = /* @__PURE__ */ new Set();
1675
+ const enums = /* @__PURE__ */ new Map();
1676
+ for (const owner of all) {
1677
+ if (namespaces.has(owner.namespace)) throw new OrmDefinitionError(`duplicate imported namespace '${owner.namespace}'`);
1678
+ namespaces.add(owner.namespace);
1679
+ for (const value of owner.enums) {
1680
+ const previous = enums.get(value.name);
1681
+ if (previous && stableJson(previous) !== stableJson(value)) {
1682
+ throw new OrmDefinitionError(`conflicting imported enum '${value.name}'`);
1683
+ }
1684
+ enums.set(value.name, value);
1685
+ }
1686
+ for (const relation of canonicalRelations(owner)) {
1687
+ if (owners.has(relation.table)) throw new OrmDefinitionError(`ambiguous imported relation '${relation.table}'`);
1688
+ owners.set(relation.table, owner);
1689
+ }
1690
+ }
1691
+ for (const owner of definition.imports?.length ? all : []) {
1692
+ for (const relation of canonicalRelations(owner)) {
1693
+ for (const link of relation.links) {
1694
+ if (link.kind === "toPlatformUser") continue;
1695
+ const target = owners.get(link.target ?? "");
1696
+ if (!target || target.namespace !== (link.namespace ?? owner.namespace)) {
1697
+ throw new OrmDefinitionError(`link '${owner.namespace}/${relation.table}.${link.name}' has no matching imported target`);
1698
+ }
1699
+ if (target.namespace !== owner.namespace && !target.exports?.[link.target ?? ""]) {
1700
+ throw new OrmDefinitionError(`target '${target.namespace}/${link.target}' is not exported`);
1701
+ }
1702
+ }
1703
+ }
1704
+ }
1705
+ return {
1706
+ owners,
1707
+ definition: {
1708
+ ...localContract(definition),
1709
+ tables: all.flatMap((item) => item.tables),
1710
+ views: all.flatMap((item) => item.views ?? []),
1711
+ enums: [...enums.values()]
1712
+ }
1713
+ };
1714
+ }
1715
+
878
1716
  // src/sdl/generate.ts
879
1717
  var SdlGenerationError = class extends Error {
880
1718
  constructor(message) {
@@ -895,8 +1733,10 @@ var PLATFORM_USER_FIELDS = [
895
1733
  { name: "accountEnabled", gqlType: "Boolean", nullable: false }
896
1734
  ];
897
1735
  function generateSdl(canonical, options = {}) {
898
- const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
899
- const tables = canonicalRelations(def).filter((table) => table.source === void 0);
1736
+ const def = composeNamespace(normalizeCanonical(canonicalDefinitionSchema.parse(canonical))).definition;
1737
+ const exposed = canonicalRelations(def).filter((table) => table.source === void 0 && table.internal !== true);
1738
+ const cellsByTable = new Map(exposed.map((table) => [table.table, trackedCells(table)]));
1739
+ const tables = exposed.map((table) => ({ ...table, columns: table.columns.filter((column) => column.cell === void 0) }));
900
1740
  if (tables.length === 0) {
901
1741
  throw new SdlGenerationError(
902
1742
  `Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
@@ -955,7 +1795,9 @@ function generateSdl(canonical, options = {}) {
955
1795
  blocks.push(
956
1796
  '"""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'
957
1797
  );
1798
+ if (tables.some((table) => table.resolved !== void 0)) blocks.push(...RESOLVED_DIRECTIVE_BLOCKS);
958
1799
  for (const scalar of CUSTOM_SCALARS) blocks.push(`scalar ${scalar}`);
1800
+ blocks.push(...renderSharedCellTypes([...cellsByTable.values()]));
959
1801
  blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
960
1802
  blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
961
1803
  for (const enumName of [...usedEnums].sort()) {
@@ -964,11 +1806,12 @@ function generateSdl(canonical, options = {}) {
964
1806
  for (const value of entry.values) assertEnumValue(value, `enum "${enumName}"`);
965
1807
  blocks.push(renderEnum(enumTypeName(enumName), entry.values));
966
1808
  }
1809
+ const patternTypes = options.stringOnlyPatternOperators ? STRING_PATTERN_COMPARISON_TYPES : PATTERN_COMPARISON_TYPES;
967
1810
  for (const scalar of [...usedComparisonScalars].sort()) {
968
- blocks.push(renderComparisonExp(scalar));
1811
+ blocks.push(renderComparisonExp({ gqlTypeName: scalar, patternTypes }));
969
1812
  }
970
1813
  for (const enumName of [...usedEnums].sort()) {
971
- blocks.push(renderComparisonExp(enumTypeName(enumName)));
1814
+ blocks.push(renderComparisonExp({ gqlTypeName: enumTypeName(enumName), patternTypes }));
972
1815
  }
973
1816
  if (hasPlatformUser) blocks.push(...renderPlatformUserBlocks());
974
1817
  for (const table of tables) {
@@ -980,7 +1823,8 @@ function generateSdl(canonical, options = {}) {
980
1823
  names,
981
1824
  namesByTable,
982
1825
  emitAggregateBoolExp: aggregateBoolExpTargets.has(table.table),
983
- pinned: options.pinnedColumns?.[table.table] ?? []
1826
+ pinned: options.pinnedColumns?.[table.table] ?? [],
1827
+ cells: cellsByTable.get(table.table) ?? null
984
1828
  })
985
1829
  );
986
1830
  }
@@ -1021,7 +1865,9 @@ function descriptionLines(text, indent) {
1021
1865
  const escaped = text.replace(/"""/g, '\\"""');
1022
1866
  return [`${indent}"""`, ...escaped.split("\n").map((line) => `${indent}${line}`), `${indent}"""`];
1023
1867
  }
1024
- function renderComparisonExp(gqlTypeName) {
1868
+ var PATTERN_COMPARISON_TYPES = /* @__PURE__ */ new Set(["String", "Int", "bigint", "uuid"]);
1869
+ var STRING_PATTERN_COMPARISON_TYPES = /* @__PURE__ */ new Set(["String"]);
1870
+ function renderComparisonExp({ gqlTypeName, patternTypes }) {
1025
1871
  const fields = [
1026
1872
  ["_eq", gqlTypeName],
1027
1873
  ["_neq", gqlTypeName],
@@ -1033,7 +1879,7 @@ function renderComparisonExp(gqlTypeName) {
1033
1879
  ["_nin", `[${gqlTypeName}!]`],
1034
1880
  ["_isNull", "Boolean"]
1035
1881
  ];
1036
- if (gqlTypeName === "String") {
1882
+ if (patternTypes.has(gqlTypeName)) {
1037
1883
  fields.push(["_like", "String"], ["_ilike", "String"]);
1038
1884
  }
1039
1885
  if (gqlTypeName === "jsonb") {
@@ -1070,6 +1916,52 @@ ${orderFields}
1070
1916
  }`
1071
1917
  ];
1072
1918
  }
1919
+ var RESOLVED_DIRECTIVE_BLOCKS = [
1920
+ '"""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',
1921
+ renderEnum("WritebackMode", ["SYNC", "ASYNC"]),
1922
+ '"""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',
1923
+ '"""Declare that this mutation deliberately keeps writeback columns local to the named source."""\ndirective @localOnly(source: String!) repeatable on FIELD'
1924
+ ];
1925
+ function trackedCells(table) {
1926
+ const columns = [...new Set(table.columns.flatMap((column) => column.cell ? [column.cell.of] : []))];
1927
+ if (columns.length === 0) return null;
1928
+ return { resolved: table.columns.some((column) => column.cell?.part === "source"), columns };
1929
+ }
1930
+ function renderSharedCellTypes(all) {
1931
+ const present = all.filter((cells) => cells !== null);
1932
+ const blocks = [];
1933
+ if (present.some((cells) => !cells.resolved)) {
1934
+ blocks.push(`"""When, and by whom, a cell was last written."""
1935
+ type ${CELL_TYPENAMES.stamp} {
1936
+ at: timestamptz!
1937
+ by: String
1938
+ }`);
1939
+ }
1940
+ if (present.some((cells) => cells.resolved)) {
1941
+ blocks.push(`"""A newer candidate value that lost to the column's resolve rule."""
1942
+ type ${CELL_TYPENAMES.conflict} {
1943
+ source: String!
1944
+ at: timestamptz
1945
+ value: jsonb
1946
+ }`);
1947
+ blocks.push(`"""How a resolved cell was decided: the winning source, its time and actor, and newer losers."""
1948
+ type ${CELL_TYPENAMES.resolved} {
1949
+ source: String!
1950
+ at: timestamptz
1951
+ by: String
1952
+ conflicts: [${CELL_TYPENAMES.conflict}!]!
1953
+ }`);
1954
+ }
1955
+ return blocks;
1956
+ }
1957
+ function renderCellsType(ctx) {
1958
+ if (!ctx.cells) return [];
1959
+ const cellType = ctx.cells.resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp;
1960
+ const lines = ctx.cells.columns.map((column) => ` ${fieldName(column)}: ${cellType}`);
1961
+ return [`type ${cellsTypename(ctx.names.typename)} {
1962
+ ${lines.join("\n")}
1963
+ }`];
1964
+ }
1073
1965
  function relationArgs(target) {
1074
1966
  return `where: ${target.boolExp}, orderBy: [${target.orderBy}!], limit: Int, offset: Int`;
1075
1967
  }
@@ -1078,10 +1970,10 @@ function rootArgs(names) {
1078
1970
  }
1079
1971
  function primaryKeyColumns(table) {
1080
1972
  if (!table.primaryKey || table.primaryKey.length === 0) return [];
1081
- return table.primaryKey.map((identifier2) => {
1082
- const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
1973
+ return table.primaryKey.map((identifier3) => {
1974
+ const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
1083
1975
  if (!column) {
1084
- throw new SdlGenerationError(`Primary key column "${identifier2}" not found on table "${table.table}".`);
1976
+ throw new SdlGenerationError(`Primary key column "${identifier3}" not found on table "${table.table}".`);
1085
1977
  }
1086
1978
  return column;
1087
1979
  });
@@ -1090,6 +1982,7 @@ function renderTableBlocks(ctx) {
1090
1982
  const { table, names } = ctx;
1091
1983
  const blocks = [
1092
1984
  renderObjectType(ctx),
1985
+ ...renderCellsType(ctx),
1093
1986
  renderSelectColumnEnum(ctx),
1094
1987
  renderOrderByInput(ctx),
1095
1988
  renderBoolExp(ctx),
@@ -1104,11 +1997,49 @@ function renderTableBlocks(ctx) {
1104
1997
  ${fields}
1105
1998
  }`);
1106
1999
  }
1107
- if (allowsInsert(table) || allowsUpdate(table)) {
2000
+ if (ctx.table.resolved !== void 0) {
2001
+ blocks.push(...renderResolvedWriteBlocks(ctx));
2002
+ } else if (allowsInsert(table) || allowsUpdate(table)) {
1108
2003
  blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
1109
2004
  }
1110
2005
  return blocks;
1111
2006
  }
2007
+ function resolvedWritableColumns(table) {
2008
+ const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
2009
+ return table.columns.filter((column) => !system.has(column.name));
2010
+ }
2011
+ function renderResolvedWriteBlocks(ctx) {
2012
+ const { table, names } = ctx;
2013
+ const writable = resolvedWritableColumns(table);
2014
+ const pk = new Set(table.primaryKey ?? []);
2015
+ const settable = writable.filter((column) => !pk.has(column.columnName));
2016
+ const line = (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`;
2017
+ const blocks = [];
2018
+ if (allowsInsert(table)) blocks.push(`input ${names.insertInput} {
2019
+ ${writable.map(line).join("\n")}
2020
+ }`);
2021
+ if (allowsUpdate(table)) {
2022
+ blocks.push(`input ${names.setInput} {
2023
+ ${settable.map(line).join("\n")}
2024
+ }`);
2025
+ blocks.push(renderEnum(names.updateColumnEnum, settable.map((column) => fieldName(column.name))));
2026
+ }
2027
+ return blocks;
2028
+ }
2029
+ function renderResolvedMutationFields(table, names) {
2030
+ const lines = [];
2031
+ const pk = primaryKeyColumns(table);
2032
+ if (allowsInsert(table)) lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!): ${names.typename}`);
2033
+ if (allowsUpdate(table) && pk.length > 0) {
2034
+ lines.push(` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, _set: ${names.setInput}!): ${names.typename}`);
2035
+ lines.push(` ${names.revertByPkRoot}(pkColumns: ${names.pkColumnsInput}!, columns: [${names.updateColumnEnum}!]!): ${names.typename}`);
2036
+ if ((table.resolved?.deletedBy.length ?? 0) > 0) {
2037
+ const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
2038
+ lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
2039
+ }
2040
+ }
2041
+ return lines;
2042
+ }
1112
2043
  function allowsInsert(table) {
1113
2044
  return table.policies.write !== void 0 || table.policies.insert !== void 0;
1114
2045
  }
@@ -1131,6 +2062,10 @@ function renderObjectType(ctx) {
1131
2062
  const deprecated = column.deprecated ? " @deprecated" : "";
1132
2063
  lines.push(` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}${bang}${deprecated}`);
1133
2064
  }
2065
+ if (ctx.cells) {
2066
+ lines.push(' """Per-cell metadata: when each field was last written, and by whom."""');
2067
+ lines.push(` ${CELLS_FIELD}: ${cellsTypename(names.typename)}!`);
2068
+ }
1134
2069
  for (const link of table.links) {
1135
2070
  if (link.description !== void 0) lines.push(...descriptionLines(link.description, " "));
1136
2071
  if (link.kind === "toPlatformUser") {
@@ -1199,7 +2134,10 @@ function numericishColumns(table) {
1199
2134
  return table.columns.filter((column) => typeof column.type !== "object" && NUMERICISH.has(column.type));
1200
2135
  }
1201
2136
  function jsonbColumns(table) {
1202
- return table.columns.filter((column) => column.type === "jsonb");
2137
+ return clientWritableColumns(table).filter((column) => column.type === "jsonb");
2138
+ }
2139
+ function clientWritableColumns(table) {
2140
+ return table.columns;
1203
2141
  }
1204
2142
  function renderAggregateTypes(ctx) {
1205
2143
  const { table, names } = ctx;
@@ -1283,14 +2221,14 @@ function constraintValues(table) {
1283
2221
  return [...values].sort();
1284
2222
  }
1285
2223
  function clientSettableColumns(table) {
1286
- return table.columns.filter((column) => column.onUpdateFn === void 0);
2224
+ return clientWritableColumns(table).filter((column) => column.onUpdateFn === void 0);
1287
2225
  }
1288
2226
  function renderWriteBlocks(ctx, access) {
1289
2227
  const { table, names, pinned } = ctx;
1290
2228
  const blocks = [];
1291
2229
  if (access.insert) {
1292
2230
  const pinnedSet = new Set(pinned);
1293
- const insertable = table.columns.filter(
2231
+ const insertable = clientWritableColumns(table).filter(
1294
2232
  (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
1295
2233
  );
1296
2234
  if (insertable.length === 0) {
@@ -1383,6 +2321,10 @@ function renderMutationRoot(tables, namesByTable) {
1383
2321
  if (!canInsert && !canUpdate) continue;
1384
2322
  const names = namesByTable.get(table.table);
1385
2323
  if (!names) continue;
2324
+ if (table.resolved !== void 0) {
2325
+ lines.push(...renderResolvedMutationFields(table, names));
2326
+ continue;
2327
+ }
1386
2328
  const constraints = constraintValues(table);
1387
2329
  const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
1388
2330
  const updateArgs = [`_set: ${names.setInput}`];
@@ -1436,12 +2378,6 @@ function renderCowMutationFields(table, names) {
1436
2378
  }
1437
2379
  return lines;
1438
2380
  }
1439
- function stableJson(value) {
1440
- if (value === null || typeof value !== "object") return JSON.stringify(value);
1441
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1442
- const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
1443
- return `{${entries.join(",")}}`;
1444
- }
1445
2381
 
1446
2382
  // src/codegen-gql/errors.ts
1447
2383
  var CodegenGqlError = class extends Error {
@@ -1512,17 +2448,17 @@ function normalizeRequires(requires) {
1512
2448
 
1513
2449
  // src/codegen-gql/type-policies.ts
1514
2450
  function generateTypePolicies(canonical) {
1515
- const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
2451
+ const def = composeNamespace(normalizeCanonical(canonicalDefinitionSchema.parse(canonical))).definition;
1516
2452
  const entries = [];
1517
2453
  let hasPlatformUser = false;
1518
2454
  for (const table of canonicalRelations(def)) {
1519
- if (table.source !== void 0) continue;
2455
+ if (table.source !== void 0 || table.internal === true) continue;
1520
2456
  if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
1521
2457
  if (!table.primaryKey || table.primaryKey.length === 0) continue;
1522
- const keyFields = table.primaryKey.map((identifier2) => {
1523
- const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
2458
+ const keyFields = table.primaryKey.map((identifier3) => {
2459
+ const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
1524
2460
  if (!column) {
1525
- throw new CodegenGqlError(`Primary key column "${identifier2}" not found on relation "${table.table}".`);
2461
+ throw new CodegenGqlError(`Primary key column "${identifier3}" not found on relation "${table.table}".`);
1526
2462
  }
1527
2463
  return fieldName(column.name);
1528
2464
  });
@@ -1541,6 +2477,7 @@ ${body}
1541
2477
  }
1542
2478
 
1543
2479
  exports.BUCKETABLE_COLUMN_TYPES = BUCKETABLE_COLUMN_TYPES;
2480
+ exports.CELL_TYPENAMES = CELL_TYPENAMES;
1544
2481
  exports.CUSTOM_SCALARS = CUSTOM_SCALARS;
1545
2482
  exports.DATE_BUCKET_ENUM = DATE_BUCKET_ENUM;
1546
2483
  exports.MANIFEST_VERSION = MANIFEST_VERSION;
@@ -1552,6 +2489,7 @@ exports.SdlGenerationError = SdlGenerationError;
1552
2489
  exports.VARIABLE_SCALAR_NAMES = VARIABLE_SCALAR_NAMES;
1553
2490
  exports.buildOperationsManifest = buildOperationsManifest;
1554
2491
  exports.camelCase = camelCase;
2492
+ exports.cellsTypename = cellsTypename;
1555
2493
  exports.columnTypeForScalar = columnTypeForScalar;
1556
2494
  exports.comparisonExpName = comparisonExpName;
1557
2495
  exports.enumTypeName = enumTypeName;
@@ -1563,6 +2501,7 @@ exports.manifestOperationSchema = manifestOperationSchema;
1563
2501
  exports.manifestRequiresSchema = manifestRequiresSchema;
1564
2502
  exports.operationsManifestSchema = operationsManifestSchema;
1565
2503
  exports.pascalCase = pascalCase;
2504
+ exports.resolvedWritableColumns = resolvedWritableColumns;
1566
2505
  exports.tableNames = tableNames;
1567
2506
  exports.typeName = typeName;
1568
2507
  //# sourceMappingURL=sdl.cjs.map