@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
@@ -32,10 +32,477 @@ var PolicyCompileError = class extends Error {
32
32
 
33
33
  // src/constants.ts
34
34
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
35
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
35
+ var MAX_IDENTIFIER_LENGTH = 63;
36
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
37
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
38
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
39
+ var CELL_PART_SUFFIX = {
40
+ at: "__at",
41
+ by: "__by",
42
+ rev: "__rev",
43
+ source: "__src",
44
+ conflicts: "__lost"
45
+ };
46
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
47
+ function cellColumnName(column, part) {
48
+ return `${column}${CELL_PART_SUFFIX[part]}`;
49
+ }
50
+ var MATERIALIZE_ACTOR = "orm:materialize";
51
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
52
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
53
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
54
+
55
+ // src/checks/compile.ts
56
+ function tokenize(source) {
57
+ const tokens = [];
58
+ for (let position = 0; position < source.length; ) {
59
+ const char = source[position];
60
+ if (/\s/.test(char)) {
61
+ position++;
62
+ continue;
63
+ }
64
+ if (char === "(" || char === ")" || char === ",") {
65
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
66
+ position++;
67
+ continue;
68
+ }
69
+ if (char === "'") {
70
+ const start = position++;
71
+ let value = "";
72
+ let closed = false;
73
+ while (position < source.length) {
74
+ if (source[position] !== "'") {
75
+ value += source[position++];
76
+ continue;
77
+ }
78
+ if (source[position + 1] === "'") {
79
+ value += "'";
80
+ position += 2;
81
+ continue;
82
+ }
83
+ position++;
84
+ closed = true;
85
+ break;
86
+ }
87
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
88
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
89
+ tokens.push({ kind: "string", value, position: start });
90
+ continue;
91
+ }
92
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
93
+ if (operator) {
94
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
95
+ position += operator[0].length;
96
+ continue;
97
+ }
98
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
99
+ if (number) {
100
+ tokens.push({ kind: "number", value: number[0], position });
101
+ position += number[0].length;
102
+ continue;
103
+ }
104
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
105
+ if (identifier3) {
106
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
107
+ position += identifier3[0].length;
108
+ continue;
109
+ }
110
+ throw new Error(`unexpected character '${char}' at position ${position}`);
111
+ }
112
+ tokens.push({ kind: "end", value: "", position: source.length });
113
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
114
+ return tokens;
115
+ }
116
+ var Parser = class {
117
+ constructor(tokens, columns) {
118
+ this.tokens = tokens;
119
+ this.columns = /* @__PURE__ */ new Map();
120
+ for (const column of columns) {
121
+ for (const alias of [column.name, column.columnName]) {
122
+ const previous = this.columns.get(alias);
123
+ if (previous && previous.columnName !== column.columnName) {
124
+ throw new Error(`ambiguous check column '${alias}'`);
125
+ }
126
+ this.columns.set(alias, column);
127
+ }
128
+ }
129
+ }
130
+ index = 0;
131
+ depth = 0;
132
+ columns;
133
+ parse() {
134
+ const expression = this.parseOr();
135
+ this.expect("end", "end of expression");
136
+ return expression;
137
+ }
138
+ parseOr() {
139
+ let left = this.parseAnd();
140
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
141
+ return left;
142
+ }
143
+ parseAnd() {
144
+ let left = this.parseNot();
145
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
146
+ return left;
147
+ }
148
+ parseNot() {
149
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
150
+ return this.parsePrimary();
151
+ }
152
+ parsePrimary() {
153
+ if (this.peek().kind === "lparen") {
154
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
155
+ this.advance();
156
+ const expression = this.parseOr();
157
+ this.expect("rparen", "')'");
158
+ this.depth--;
159
+ return `(${expression})`;
160
+ }
161
+ return this.parseComparison();
162
+ }
163
+ parseComparison() {
164
+ const left = this.parseValue();
165
+ if (this.matchKeyword("is")) {
166
+ const negate = this.matchKeyword("not");
167
+ this.expectKeyword("null");
168
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
169
+ }
170
+ if (this.matchKeyword("between")) {
171
+ const lower = this.parseValue();
172
+ this.expectKeyword("and");
173
+ const upper = this.parseValue();
174
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
175
+ }
176
+ if (this.matchKeyword("in")) {
177
+ this.expect("lparen", "'(' after IN");
178
+ const values = [this.parseValue()];
179
+ while (this.peek().kind === "comma") {
180
+ this.advance();
181
+ values.push(this.parseValue());
182
+ }
183
+ this.expect("rparen", "')' after IN values");
184
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
185
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
186
+ }
187
+ const operator = this.expect("operator", "a comparison operator");
188
+ const right = this.parseValue();
189
+ if (operator.value === "~" || operator.value === "!~") {
190
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
191
+ if (!right.literal) throw new Error("regex pattern must be a literal");
192
+ }
193
+ return `${left.sql} ${operator.value} ${right.sql}`;
194
+ }
195
+ parseValue() {
196
+ const token = this.advance();
197
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
198
+ if (token.kind === "number") {
199
+ const number = Number(token.value);
200
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
201
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
202
+ }
203
+ return { sql: token.value, literal: true };
204
+ }
205
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
206
+ const keyword = token.value.toLowerCase();
207
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
208
+ if (keyword === "btrim") {
209
+ this.expect("lparen", "'(' after btrim");
210
+ const column2 = this.parseColumn();
211
+ this.expect("rparen", "')' after btrim column");
212
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
213
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
214
+ }
215
+ const column = this.columns.get(token.value);
216
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
217
+ return { sql: quoteIdentifier(column.columnName), column };
218
+ }
219
+ parseColumn() {
220
+ const token = this.expect("identifier", "a column name");
221
+ const column = this.columns.get(token.value);
222
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
223
+ return column;
224
+ }
225
+ peek() {
226
+ return this.tokens[this.index];
227
+ }
228
+ advance() {
229
+ const token = this.peek();
230
+ if (token.kind !== "end") this.index++;
231
+ return token;
232
+ }
233
+ matchKeyword(keyword) {
234
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
235
+ this.advance();
236
+ return true;
237
+ }
238
+ expectKeyword(keyword) {
239
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
240
+ }
241
+ expect(kind, description) {
242
+ const token = this.peek();
243
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
244
+ return this.advance();
245
+ }
246
+ };
247
+ function compileCheckExpression(source, columns) {
248
+ return new Parser(tokenize(source), columns).parse();
249
+ }
250
+ var policyVariantSchema = zod.z.object({
251
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
252
+ when: zod.z.string().min(1).max(1e3),
253
+ /** Exactly one of: allow-all bypass, or a row predicate. */
254
+ allow: zod.z.literal(true).optional(),
255
+ policy: zod.z.string().min(1).max(2e3).optional()
256
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
257
+ message: "A variant declares exactly one of allow: true or policy"
258
+ });
259
+ var policyExpressionSchema = zod.z.union([
260
+ zod.z.string().min(1).max(2e3),
261
+ zod.z.object({
262
+ default: zod.z.string().min(1).max(2e3),
263
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
264
+ })
265
+ ]);
266
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
267
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
268
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
269
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
270
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
271
+ };
272
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
273
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
274
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
275
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
276
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
277
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
278
+ };
279
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
280
+ /* @__PURE__ */ new Set([
281
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
282
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
283
+ ]);
284
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
285
+ var canonicalFunctionRefSchema = zod.z.object({
286
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
287
+ kind: zod.z.enum(["managed", "native"]),
288
+ id: zod.z.string().min(1).max(200)
289
+ }).strict();
290
+ var canonicalFetchShapeSchema = zod.z.union([
291
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
292
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
293
+ ]);
294
+ var canonicalResolvedSourceSchema = zod.z.object({
295
+ name: identifier,
296
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
297
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
298
+ table: identifier,
299
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
300
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
301
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
302
+ updatedAt: zod.z.string().min(1).max(63).optional(),
303
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
304
+ key: zod.z.string().min(1).max(63).optional(),
305
+ fetch: canonicalFetchShapeSchema.optional(),
306
+ function: canonicalFunctionRefSchema.optional(),
307
+ prefetchDefaults: zod.z.object({
308
+ maxStalenessMs: durationMs,
309
+ timeoutMs: durationMs,
310
+ onError: zod.z.enum(["fallback", "fail"])
311
+ }).strict().optional(),
312
+ writeback: zod.z.object({
313
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
314
+ defaultMode: zod.z.enum(["sync", "async"]),
315
+ timeoutMs: durationMs,
316
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
317
+ clearManualOnConfirm: zod.z.boolean()
318
+ }).strict().optional()
319
+ }).strict();
320
+ var canonicalResolveRuleSchema = zod.z.object({
321
+ column: zod.z.string().min(1).max(63),
322
+ comparator: zod.z.enum(COMPARATORS),
323
+ /** Source names, in tie-break order. */
324
+ candidates: zod.z.array(identifier).min(1).max(16),
325
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
326
+ orderBy: zod.z.string().min(1).max(2e3).optional()
327
+ }).strict();
328
+ var canonicalResolvedSchema = zod.z.object({
329
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
330
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
331
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
332
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
333
+ deletedBy: zod.z.array(identifier).max(1).default([]),
334
+ /**
335
+ * Materialized: the view reads an indexed internal table (`table`) that the
336
+ * data plane keeps in step with `compute`, the internal view holding the
337
+ * resolution query. Omitted when the view computes on read.
338
+ */
339
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
340
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
341
+ policies: zod.z.object({
342
+ write: policyExpressionSchema.optional(),
343
+ insert: policyExpressionSchema.optional(),
344
+ update: policyExpressionSchema.optional()
345
+ }).strict().default({})
346
+ }).strict();
347
+ function validateResolvedView({ view, tables, views = [] }) {
348
+ const resolved = view.resolved;
349
+ if (!resolved) return [];
350
+ const errors = [];
351
+ const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
352
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
353
+ if (!pkColumn) {
354
+ fail("requires exactly one primary-key column");
355
+ return errors;
356
+ }
357
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
358
+ const scope = {
359
+ view,
360
+ resolved,
361
+ pkColumn,
362
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
363
+ sourceByName: /* @__PURE__ */ new Map(),
364
+ fail
365
+ };
366
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
367
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
368
+ for (const source of resolved.sources) {
369
+ if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
370
+ scope.sourceByName.set(source.name, source);
371
+ const table = tableByName.get(source.table);
372
+ if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
373
+ else validateSource({ scope, source, table });
374
+ }
375
+ validateRules(scope);
376
+ validateViewCells(scope);
377
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
378
+ for (const name of resolved.deletedBy) {
379
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
380
+ }
381
+ return errors;
382
+ }
383
+ function validateSource({ scope, source, table }) {
384
+ const { fail } = scope;
385
+ if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
386
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
387
+ fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
388
+ }
389
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
390
+ if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
391
+ if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
392
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
393
+ validateSourceColumns({ scope, source, byName });
394
+ if (source.kind === "etl") {
395
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
396
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
397
+ fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
398
+ }
399
+ }
400
+ validatePrefetchFields({ scope, source });
401
+ }
402
+ function validatePrefetchFields({ scope, source }) {
403
+ const { fail, pkColumn } = scope;
404
+ if (source.kind !== "prefetch") {
405
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
406
+ fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
407
+ }
408
+ return;
409
+ }
410
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
411
+ fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
412
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
413
+ fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
414
+ }
415
+ for (const column of source.writeback?.columns ?? []) {
416
+ if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
417
+ }
418
+ }
419
+ function validateSourceColumns({ scope, source, byName }) {
420
+ const { fail, pkColumn, dataColumns } = scope;
421
+ for (const column of [pkColumn.name, ...source.columns]) {
422
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
423
+ const theirs = byName.get(column);
424
+ if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
425
+ else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
426
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
427
+ }
428
+ }
429
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
430
+ const ruled = /* @__PURE__ */ new Set();
431
+ for (const rule of resolved.resolve) {
432
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
433
+ fail(`resolve rule targets unknown column '${rule.column}'`);
434
+ continue;
435
+ }
436
+ if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
437
+ ruled.add(rule.column);
438
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
439
+ fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
440
+ }
441
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
442
+ for (const candidate of rule.candidates) {
443
+ const source = sourceByName.get(candidate);
444
+ if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
445
+ else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
446
+ }
447
+ }
448
+ for (const column of dataColumns.keys()) {
449
+ if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
450
+ }
451
+ }
452
+ var VIEW_CELL_TYPES = {
453
+ source: "text",
454
+ at: "timestamptz",
455
+ by: "text",
456
+ conflicts: "jsonb"
457
+ };
458
+ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
459
+ const expected = /* @__PURE__ */ new Map();
460
+ for (const column of dataColumns.values()) {
461
+ if (column.name === pkColumn.name) continue;
462
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
463
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
464
+ }
465
+ }
466
+ for (const column of view.columns) {
467
+ if (!column.cell) continue;
468
+ const want = expected.get(column.name);
469
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
470
+ fail(`unexpected cell column '${column.name}'`);
471
+ continue;
472
+ }
473
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
474
+ expected.delete(column.name);
475
+ }
476
+ for (const name of expected.keys()) fail(`missing cell column '${name}'`);
477
+ }
478
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
479
+ function validateMaterialized({ scope, tableByName, views }) {
480
+ const { view, resolved, fail } = scope;
481
+ const { table: tableName, compute: computeName } = resolved.materialized;
482
+ const table = tableByName.get(tableName);
483
+ if (!table || table.internal !== true || table.materializes !== view.view) {
484
+ fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
485
+ } else {
486
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
487
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
488
+ }
489
+ const compute = views.find((candidate) => candidate.view === computeName);
490
+ if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
491
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
492
+ for (const source of resolved.sources) {
493
+ if (source.kind !== "etl") continue;
494
+ const read = tableByName.get(source.table)?.policies?.read;
495
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
496
+ if (!unconditional) {
497
+ fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
498
+ }
499
+ }
500
+ }
501
+
502
+ // src/canonical/v2.ts
36
503
  var CANONICAL_FORMAT = 2;
37
504
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
38
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
505
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
39
506
  var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
40
507
  var COLUMN_TYPES = [
41
508
  "uuid",
@@ -51,14 +518,14 @@ var COLUMN_TYPES = [
51
518
  ];
52
519
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
53
520
  var canonicalEnumSchema = zod.z.object({
54
- name: identifier,
521
+ name: identifier2,
55
522
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
56
523
  });
57
524
  var canonicalColumnSchema = zod.z.object({
58
525
  /** Property name in TS (camelCase); columnName is the Postgres name. */
59
526
  name: zod.z.string().min(1).max(63),
60
527
  columnName: columnIdentifier,
61
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
528
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
62
529
  nullable: zod.z.boolean().default(true),
63
530
  unique: zod.z.boolean().default(false),
64
531
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -74,7 +541,14 @@ var canonicalColumnSchema = zod.z.object({
74
541
  /** Migration hints — same semantics as v1. */
75
542
  renamedFrom: columnIdentifier.optional(),
76
543
  cast: zod.z.string().max(200).optional(),
77
- deprecated: zod.z.boolean().default(false)
544
+ deprecated: zod.z.boolean().default(false),
545
+ /**
546
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
547
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
548
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
549
+ * ordinary columns so their hashes are unchanged.
550
+ */
551
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
78
552
  }).superRefine((column, ctx) => {
79
553
  if (column.onUpdateFn === void 0) return;
80
554
  if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
@@ -89,36 +563,28 @@ var canonicalLinkSchema = zod.z.object({
89
563
  name: zod.z.string().min(1).max(63),
90
564
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
91
565
  /** Target table name (namespace-local). Absent for toPlatformUser. */
92
- target: identifier.optional(),
566
+ target: identifier2.optional(),
567
+ /** Owning namespace of an imported target; omitted for local links. */
568
+ namespace: identifier2.max(41).optional(),
93
569
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
94
570
  on: columnIdentifier,
95
571
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
96
572
  references: columnIdentifier.optional(),
97
573
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
98
574
  enforce: zod.z.boolean().default(false),
575
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
576
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
99
577
  description: zod.z.string().max(500).optional()
100
578
  });
101
579
  var canonicalIndexSchema = zod.z.object({
102
580
  columns: zod.z.array(columnIdentifier).min(1),
103
581
  unique: zod.z.boolean().default(false),
104
- name: identifier.optional()
582
+ name: identifier2.optional()
105
583
  });
106
- var policyVariantSchema = zod.z.object({
107
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
108
- when: zod.z.string().min(1).max(1e3),
109
- /** Exactly one of: allow-all bypass, or a row predicate. */
110
- allow: zod.z.literal(true).optional(),
111
- policy: zod.z.string().min(1).max(2e3).optional()
112
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
113
- message: "A variant declares exactly one of allow: true or policy"
114
- });
115
- var policyExpressionSchema = zod.z.union([
116
- zod.z.string().min(1).max(2e3),
117
- zod.z.object({
118
- default: zod.z.string().min(1).max(2e3),
119
- variants: zod.z.array(policyVariantSchema).max(10).default([])
120
- })
121
- ]);
584
+ var canonicalCheckSchema = zod.z.object({
585
+ name: identifier2,
586
+ expression: zod.z.string().min(1).max(2e3)
587
+ }).strict();
122
588
  var canonicalPoliciesSchema = zod.z.object({
123
589
  read: policyExpressionSchema.optional(),
124
590
  write: policyExpressionSchema.optional(),
@@ -149,7 +615,7 @@ function isSingleViewQuerySql(value) {
149
615
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
150
616
  }
151
617
  var canonicalActionSchema = zod.z.object({
152
- name: identifier,
618
+ name: identifier2,
153
619
  requires: zod.z.string().max(1e3).optional(),
154
620
  params: zod.z.record(zod.z.string(), zod.z.object({
155
621
  type: columnTypeSchema,
@@ -161,9 +627,9 @@ var canonicalActionSchema = zod.z.object({
161
627
  });
162
628
  var canonicalCowMutationSchema = zod.z.object({
163
629
  kind: zod.z.enum(["override", "revert"]),
164
- name: identifier,
630
+ name: identifier2,
165
631
  /** Synced table (same namespace) the overlay copies from / falls back to. */
166
- synced: identifier,
632
+ synced: identifier2,
167
633
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
168
634
  pk: zod.z.string().min(1).max(63),
169
635
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -191,23 +657,28 @@ var canonicalCowMutationSchema = zod.z.object({
191
657
  }
192
658
  });
193
659
  var canonicalForeignKeySchema = zod.z.object({
194
- name: identifier,
660
+ name: identifier2,
195
661
  columns: zod.z.array(columnIdentifier).min(1).max(8),
196
- target: identifier,
662
+ target: identifier2,
197
663
  references: zod.z.array(columnIdentifier).min(1).max(8),
198
- deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
664
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
665
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
666
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
199
667
  }).strict();
200
668
  var canonicalTableSchema = zod.z.object({
201
- table: identifier,
669
+ table: identifier2,
670
+ displayName: zod.z.string().trim().min(1).max(120).optional(),
202
671
  description: zod.z.string().max(2e3).optional(),
203
672
  /** Table-level rename hint (new in v2). */
204
- renamedFrom: identifier.optional(),
205
- /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
206
- source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
673
+ renamedFrom: identifier2.optional(),
674
+ /** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
675
+ source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
207
676
  columns: zod.z.array(canonicalColumnSchema).default([]),
208
677
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
209
678
  links: zod.z.array(canonicalLinkSchema).default([]),
210
679
  indexes: zod.z.array(canonicalIndexSchema).default([]),
680
+ /** Omitted by old definitions, preserving their existing content hashes. */
681
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
211
682
  policies: canonicalPoliciesSchema.default({}),
212
683
  /**
213
684
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -218,22 +689,55 @@ var canonicalTableSchema = zod.z.object({
218
689
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
219
690
  cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
220
691
  /** Omitted by old definitions, preserving their existing content hashes. */
221
- foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
692
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
693
+ /**
694
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
695
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
696
+ * on a resolved table's internal sources), listed explicitly in `columns`
697
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
698
+ * Omitted when off so existing definitions hash unchanged.
699
+ */
700
+ cellMetadata: zod.z.literal(true).optional(),
701
+ /**
702
+ * System-owned backing table of a resolved table (its manual or prefetch
703
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
704
+ * only the data plane writes them. Omitted when false.
705
+ */
706
+ internal: zod.z.literal(true).optional(),
707
+ /**
708
+ * The materialized copy of resolved table `materializes` (internal): the
709
+ * same columns as that view, including its cell parts, maintained by the
710
+ * data plane. Omitted otherwise.
711
+ */
712
+ materializes: identifier2.optional()
222
713
  });
714
+ var MAX_VIEW_QUERY = 1e5;
715
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
223
716
  var canonicalViewSchema = zod.z.object({
224
- view: identifier,
717
+ view: identifier2,
225
718
  description: zod.z.string().max(2e3).optional(),
226
719
  columns: zod.z.array(canonicalColumnSchema).min(1),
227
720
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
228
721
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
229
722
  links: zod.z.array(canonicalLinkSchema).default([]),
230
723
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
231
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
724
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
232
725
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
233
726
  }),
234
727
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
235
- dependencies: zod.z.array(identifier).max(500)
728
+ dependencies: zod.z.array(identifier2).max(500),
729
+ /**
730
+ * Present when this view is a resolved table (defineResolvedTable): the
731
+ * multi-source metadata behind its generated query. Omitted otherwise so
732
+ * existing views hash unchanged.
733
+ */
734
+ resolved: canonicalResolvedSchema.optional(),
735
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
736
+ internal: zod.z.literal(true).optional()
236
737
  }).superRefine((view, ctx) => {
738
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
739
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
740
+ }
237
741
  for (const [index, column] of view.columns.entries()) {
238
742
  if (column.default !== void 0 || column.defaultFn !== void 0) {
239
743
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
@@ -248,20 +752,91 @@ var canonicalViewSchema = zod.z.object({
248
752
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
249
753
  }
250
754
  }
755
+ if (view.resolved === void 0 && view.internal !== true) {
756
+ for (const [index, column] of view.columns.entries()) {
757
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
758
+ }
759
+ }
251
760
  for (const [index, link] of view.links.entries()) {
252
761
  if (link.enforce) {
253
762
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
254
763
  }
764
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
765
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
766
+ }
255
767
  }
256
768
  });
257
- zod.z.object({
769
+ var CELL_PART_TYPES = {
770
+ at: "timestamptz",
771
+ by: "text",
772
+ rev: "uuid",
773
+ source: "text",
774
+ conflicts: "jsonb"
775
+ };
776
+ function trackedCellColumns(table) {
777
+ const pk = new Set(table.primaryKey ?? []);
778
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
779
+ }
780
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
781
+ function validateCellMetadata(table, index, ctx) {
782
+ const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
783
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
784
+ if (table.materializes !== void 0) return;
785
+ if (table.cellMetadata !== true) {
786
+ for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
787
+ return;
788
+ }
789
+ if (table.source !== void 0) {
790
+ fail(`synced table '${table.table}' cannot track cell metadata`);
791
+ return;
792
+ }
793
+ const tracked = trackedCellColumns(table);
794
+ const byName = new Map(tracked.map((column) => [column.name, column]));
795
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
796
+ if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
797
+ const seen = /* @__PURE__ */ new Set();
798
+ for (const { column, position, cell } of cells) {
799
+ const path = ["tables", index, "columns", position];
800
+ const owner = byName.get(cell.of);
801
+ if (!owner) {
802
+ fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
803
+ continue;
804
+ }
805
+ 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);
806
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
807
+ fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
808
+ }
809
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
810
+ fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
811
+ }
812
+ const key = `${cell.of}:${cell.part}`;
813
+ if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
814
+ seen.add(key);
815
+ }
816
+ for (const column of tracked) {
817
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
818
+ fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
819
+ }
820
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
821
+ fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
822
+ }
823
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
824
+ if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
825
+ }
826
+ }
827
+ }
828
+ 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()]);
829
+ var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
830
+ var localDefinitionSchema = zod.z.object({
258
831
  definition_format: zod.z.literal(CANONICAL_FORMAT),
259
- namespace: identifier.max(41),
832
+ namespace: identifier2.max(41),
260
833
  description: zod.z.string().max(2e3).optional(),
261
834
  enums: zod.z.array(canonicalEnumSchema).default([]),
262
835
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
263
836
  /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
264
837
  views: zod.z.array(canonicalViewSchema).max(500).optional(),
838
+ /** Target-owned cross-namespace access contract, using existing namespace grants. */
839
+ exports: zod.z.record(identifier2, relationExportSchema).optional(),
265
840
  /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
266
841
  capabilities: zod.z.object({
267
842
  roles: zod.z.unknown().optional(),
@@ -274,6 +849,7 @@ zod.z.object({
274
849
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
275
850
  }
276
851
  seen.add(table.table);
852
+ validateCellMetadata(table, index, ctx);
277
853
  if (table.source !== void 0) {
278
854
  for (const [columnIndex, column] of table.columns.entries()) {
279
855
  if (column.onUpdateFn !== void 0) {
@@ -287,12 +863,41 @@ zod.z.object({
287
863
  }
288
864
  }
289
865
  const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
866
+ const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
867
+ for (const name of Object.keys(definition.exports ?? {})) {
868
+ if (!relations.has(name)) ctx.addIssue({ code: "custom", path: ["exports", name], message: `export '${name}' is not a local object` });
869
+ }
290
870
  for (const [tableIndex, table] of definition.tables.entries()) {
871
+ for (const [linkIndex, link] of table.links.entries()) {
872
+ if (link.enforce && link.namespace !== void 0 && link.namespace !== definition.namespace) {
873
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", linkIndex], message: "physical foreign keys require a local target; use an ORM link without enforce" });
874
+ }
875
+ }
291
876
  const names = /* @__PURE__ */ new Set([
292
877
  ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
293
878
  ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
294
879
  ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
295
880
  ]);
881
+ for (const [index, link] of table.links.entries()) {
882
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
883
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
884
+ }
885
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
886
+ const source = table.columns.find((column) => column.columnName === link.on);
887
+ 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" });
888
+ }
889
+ }
890
+ for (const [index, check] of (table.checks ?? []).entries()) {
891
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
892
+ if (table.source) fail("synced tables cannot declare checks");
893
+ if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
894
+ names.add(check.name);
895
+ try {
896
+ compileCheckExpression(check.expression, table.columns);
897
+ } catch (error) {
898
+ fail(error instanceof Error ? error.message : String(error));
899
+ }
900
+ }
296
901
  for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
297
902
  const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
298
903
  if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
@@ -310,10 +915,18 @@ zod.z.object({
310
915
  if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
311
916
  else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
312
917
  }
918
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
919
+ 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");
920
+ }
313
921
  const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
314
922
  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");
315
923
  }
316
924
  }
925
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
926
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
927
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
928
+ }
929
+ }
317
930
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
318
931
  for (const [tableIndex, table] of definition.tables.entries()) {
319
932
  const cowMutations = table.cowMutations ?? [];
@@ -429,11 +1042,18 @@ zod.z.object({
429
1042
  };
430
1043
  for (const view of views) visit(view.view, []);
431
1044
  });
1045
+ localDefinitionSchema.safeExtend({
1046
+ /** Flattened, pinned snapshots. Imported relations never participate in this namespace's DDL. */
1047
+ imports: zod.z.array(localDefinitionSchema).max(32).optional()
1048
+ });
432
1049
 
433
1050
  // src/ir/index.ts
434
- var IR_VERSION = 2;
435
1051
  var LEGACY_IR_VERSION = 1;
436
- var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
1052
+ var GUARDED_IR_VERSION = 2;
1053
+ var COMPOSED_IR_VERSION = 3;
1054
+ var SQL_IR_VERSION = 4;
1055
+ var RUNTIME_SQL_IR_VERSION = 5;
1056
+ 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)]);
437
1057
  zod.z.object({ irVersion: irVersionSchema });
438
1058
  var CLAIMS_CONTRACT = {
439
1059
  sub: "string",
@@ -477,8 +1097,8 @@ var predIrSchema = zod.z.lazy(
477
1097
  value: zod.z.number(),
478
1098
  where: predIrSchema.optional()
479
1099
  }),
480
- zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string() }),
481
- zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string() }),
1100
+ zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string(), namespace: zod.z.string().optional() }),
1101
+ zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string(), namespace: zod.z.string().optional() }),
482
1102
  zod.z.object({ kind: zod.z.literal("dynamicFilter"), slot: zod.z.string() }),
483
1103
  zod.z.object({ kind: zod.z.literal("const"), value: zod.z.boolean() })
484
1104
  ])
@@ -506,6 +1126,21 @@ var selectionIrSchema = zod.z.lazy(
506
1126
  () => zod.z.object({
507
1127
  typename: zod.z.string(),
508
1128
  columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
1129
+ cells: zod.z.object({
1130
+ alias: zod.z.string(),
1131
+ typename: zod.z.string(),
1132
+ fields: zod.z.array(zod.z.object({
1133
+ alias: zod.z.string(),
1134
+ column: zod.z.string(),
1135
+ typename: zod.z.string(),
1136
+ parts: zod.z.array(zod.z.object({
1137
+ alias: zod.z.string(),
1138
+ part: zod.z.enum(["source", "at", "by", "conflicts"]),
1139
+ fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
1140
+ typename: zod.z.string().optional()
1141
+ }))
1142
+ }))
1143
+ }).optional(),
509
1144
  relations: zod.z.array(zod.z.object({
510
1145
  alias: zod.z.string(),
511
1146
  link: zod.z.string(),
@@ -527,6 +1162,11 @@ var selectionIrSchema = zod.z.lazy(
527
1162
  })
528
1163
  );
529
1164
  var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
1165
+ var prefetchPlanIrSchema = zod.z.object({
1166
+ source: zod.z.string(),
1167
+ maxStalenessMs: zod.z.number().int().positive().optional(),
1168
+ key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
1169
+ });
530
1170
  var selectRootSchema = zod.z.object({
531
1171
  kind: zod.z.literal("select"),
532
1172
  alias: zod.z.string(),
@@ -538,7 +1178,8 @@ var selectRootSchema = zod.z.object({
538
1178
  distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
539
1179
  limit: limitIrSchema,
540
1180
  offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
541
- includeIf: includeIfSchema.optional()
1181
+ includeIf: includeIfSchema.optional(),
1182
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
542
1183
  });
543
1184
  var byPkRootSchema = zod.z.object({
544
1185
  kind: zod.z.literal("byPk"),
@@ -548,7 +1189,8 @@ var byPkRootSchema = zod.z.object({
548
1189
  pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
549
1190
  /** Policy predicate — ANDed with the PK match. */
550
1191
  where: predIrSchema,
551
- includeIf: includeIfSchema.optional()
1192
+ includeIf: includeIfSchema.optional(),
1193
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
552
1194
  });
553
1195
  var aggregateRootSchema = zod.z.object({
554
1196
  kind: zod.z.literal("aggregate"),
@@ -606,7 +1248,20 @@ var mutationRootSchema = zod.z.object({
606
1248
  * coalesce view falls back to synced. Both are gated on a matching synced
607
1249
  * row and carry the write policy in `where` like update/delete.
608
1250
  */
609
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
1251
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
1252
+ /**
1253
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
1254
+ * executor gates on the resolved row (write policy in `where`), writes only
1255
+ * the touched cells into the manual source, then re-checks `insertCheck`
1256
+ * against the resolved row. `revert` clears `revertColumns` from the manual
1257
+ * source; `delete` tombstones the row there.
1258
+ */
1259
+ resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
1260
+ revertColumns: zod.z.array(zod.z.string()).optional(),
1261
+ /** `@writeback(source:, mode:)` directives on this root. */
1262
+ writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
1263
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
1264
+ localOnly: zod.z.array(zod.z.string()).optional(),
610
1265
  alias: zod.z.string(),
611
1266
  table: zod.z.string(),
612
1267
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -670,7 +1325,31 @@ var variableIrSchema = zod.z.object({
670
1325
  /** Absent = no default declared; see {@link variableDefaultIrSchema}. */
671
1326
  defaultValue: variableDefaultIrSchema.optional()
672
1327
  });
1328
+ 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() });
673
1329
  var compiledOperationSchema = zod.z.object({
1330
+ language: zod.z.literal("sql").optional(),
1331
+ /** Server-compiled object catalog and permission ceiling, never caller SQL. */
1332
+ sqlQuery: zod.z.object({
1333
+ catalog: zod.z.object({
1334
+ identity: zod.z.string(),
1335
+ objects: zod.z.array(zod.z.object({
1336
+ ref: zod.z.string(),
1337
+ table: zod.z.string(),
1338
+ columns: zod.z.array(sqlColumnSchema.extend({ type: zod.z.string() })).min(1),
1339
+ scan: zod.z.string().min(1),
1340
+ read: zod.z.array(zod.z.string()).min(1)
1341
+ }).strict()).min(1).max(256)
1342
+ }).strict(),
1343
+ read: zod.z.array(zod.z.string()).min(1).max(256),
1344
+ maxRows: zod.z.number().int().min(1).max(1e4)
1345
+ }).strict().optional(),
1346
+ sql: zod.z.object({
1347
+ text: zod.z.string().min(1).max(1048576),
1348
+ columns: zod.z.array(sqlColumnSchema).min(1).max(256),
1349
+ parameters: zod.z.array(zod.z.string()).max(128),
1350
+ read: zod.z.array(zod.z.string()).min(1).max(256),
1351
+ maxRows: zod.z.number().int().min(1).max(1e4)
1352
+ }).strict().optional(),
674
1353
  irVersion: irVersionSchema,
675
1354
  name: zod.z.string(),
676
1355
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
@@ -682,8 +1361,16 @@ var compiledOperationSchema = zod.z.object({
682
1361
  variables: zod.z.array(variableIrSchema),
683
1362
  /** Every table the op touches — data-token scoping + audit. */
684
1363
  tableSet: zod.z.array(zod.z.string()).min(1),
685
- roots: zod.z.array(rootIrSchema).min(1)
1364
+ roots: zod.z.array(rootIrSchema)
686
1365
  }).superRefine((operation, ctx) => {
1366
+ if (operation.language === "sql") {
1367
+ const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
1368
+ if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !operation.requires?.capability) {
1369
+ ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
1370
+ }
1371
+ } else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
1372
+ ctx.addIssue({ code: "custom", message: "GraphQL operations require roots and cannot carry SQL" });
1373
+ }
687
1374
  for (const [index, root] of operation.roots.entries()) {
688
1375
  if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
689
1376
  if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
@@ -691,13 +1378,55 @@ var compiledOperationSchema = zod.z.object({
691
1378
  }
692
1379
  }
693
1380
  });
1381
+ var resolvedSourceIrSchema = zod.z.object({
1382
+ name: zod.z.string(),
1383
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
1384
+ /** Logical table key of the backing relation. */
1385
+ table: zod.z.string(),
1386
+ columns: zod.z.array(zod.z.string()),
1387
+ key: zod.z.string().optional(),
1388
+ fetch: zod.z.union([
1389
+ zod.z.object({ shape: zod.z.literal("single") }),
1390
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
1391
+ ]).optional(),
1392
+ function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
1393
+ prefetchDefaults: zod.z.object({
1394
+ maxStalenessMs: zod.z.number().int().positive(),
1395
+ timeoutMs: zod.z.number().int().positive(),
1396
+ onError: zod.z.enum(["fallback", "fail"])
1397
+ }).optional(),
1398
+ writeback: zod.z.object({
1399
+ columns: zod.z.array(zod.z.string()),
1400
+ defaultMode: zod.z.enum(["sync", "async"]),
1401
+ timeoutMs: zod.z.number().int().positive(),
1402
+ clearManualOnConfirm: zod.z.boolean()
1403
+ }).optional()
1404
+ });
1405
+ var resolvedMetaIrSchema = zod.z.object({
1406
+ /** Logical pk column of the resolved view (single-column). */
1407
+ pk: zod.z.string(),
1408
+ /** Source name of the manual source. */
1409
+ manual: zod.z.string(),
1410
+ sources: zod.z.array(resolvedSourceIrSchema),
1411
+ deletedBy: zod.z.array(zod.z.string()),
1412
+ /**
1413
+ * Materialized: the view reads `table`, which the data plane refreshes from
1414
+ * the `compute` view (claims-gated) whenever it writes a source row.
1415
+ */
1416
+ materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
1417
+ });
694
1418
  var tableMetaIrSchema = zod.z.object({
695
1419
  table: zod.z.string(),
1420
+ namespace: zod.z.string().optional(),
1421
+ dependencies: zod.z.array(zod.z.string()).optional(),
1422
+ foreignReferences: zod.z.array(zod.z.object({ column: zod.z.string(), target: zod.z.string(), targetColumn: zod.z.string() })).optional(),
1423
+ exportRead: zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string() }), zod.z.object({ role: zod.z.string() })]).optional(),
1424
+ exportWrite: zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string() }), zod.z.object({ role: zod.z.string() })]).optional(),
696
1425
  schema: zod.z.string(),
697
1426
  typename: zod.z.string(),
698
1427
  /**
699
1428
  * True when statements over this relation evaluate a claims-gated synced
700
- * view: synced twins (`source: gold.*`) materialize as views whose baked
1429
+ * view: synced twins (`source: schema.table`) materialize as views whose baked
701
1430
  * policy reads `current_setting('request.jwt.claims')`, and namespace views
702
1431
  * inherit the gate transitively through their declared dependencies. The
703
1432
  * executor must stamp that GUC (transaction-scoped) before any statement
@@ -731,6 +1460,22 @@ var tableMetaIrSchema = zod.z.object({
731
1460
  * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
732
1461
  */
733
1462
  touchOnUpdate: zod.z.array(zod.z.string()).optional(),
1463
+ /**
1464
+ * Per-cell metadata: logical column → the logical names of its cell-part
1465
+ * columns (which are listed in `columns`, but never filterable, orderable or
1466
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
1467
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
1468
+ * the relation tracks no cells.
1469
+ */
1470
+ cells: zod.z.record(zod.z.string(), zod.z.object({
1471
+ at: zod.z.string().optional(),
1472
+ by: zod.z.string().optional(),
1473
+ rev: zod.z.string().optional(),
1474
+ source: zod.z.string().optional(),
1475
+ conflicts: zod.z.string().optional()
1476
+ }).strict()).optional(),
1477
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
1478
+ resolved: resolvedMetaIrSchema.optional(),
734
1479
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
735
1480
  /** Depth/node caps for dynamic filter trees on this table. */
736
1481
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
@@ -740,10 +1485,18 @@ zod.z.object({
740
1485
  irVersion: irVersionSchema,
741
1486
  namespace: zod.z.string(),
742
1487
  schemaHash: zod.z.string(),
1488
+ ownSchemaHash: zod.z.string().optional(),
1489
+ dependencies: zod.z.record(zod.z.string(), zod.z.string()).optional(),
1490
+ readPolicies: zod.z.record(zod.z.string(), predIrSchema).optional(),
743
1491
  opsHash: zod.z.string(),
1492
+ /** Apply-observed synced objects without a serving backing; not part of the authored schema hash. */
1493
+ unbackedTables: zod.z.array(zod.z.string()).optional(),
744
1494
  tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
745
1495
  operations: zod.z.record(zod.z.string(), compiledOperationSchema)
746
1496
  }).superRefine((artifact, ctx) => {
1497
+ if (Object.keys(artifact.dependencies ?? {}).length > 0 && artifact.irVersion < COMPOSED_IR_VERSION) {
1498
+ ctx.addIssue({ code: "custom", path: ["irVersion"], message: "cross-namespace dependencies require IR v3" });
1499
+ }
747
1500
  for (const [name, operation] of Object.entries(artifact.operations)) {
748
1501
  if (operation.irVersion > artifact.irVersion) {
749
1502
  ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
@@ -759,7 +1512,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
759
1512
  var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
760
1513
  var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
761
1514
  var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
762
- function tokenize(source) {
1515
+ function tokenize2(source) {
763
1516
  const tokens = [];
764
1517
  let position = 0;
765
1518
  while (position < source.length) {
@@ -838,7 +1591,7 @@ function readString(source, start) {
838
1591
  }
839
1592
  throw new PolicyParseError("unterminated string literal", start);
840
1593
  }
841
- var Parser = class {
1594
+ var Parser2 = class {
842
1595
  constructor(tokens) {
843
1596
  this.tokens = tokens;
844
1597
  }
@@ -1012,7 +1765,7 @@ var Parser = class {
1012
1765
  function parsePolicy(source) {
1013
1766
  const trimmed = source.trim();
1014
1767
  if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
1015
- return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
1768
+ return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
1016
1769
  }
1017
1770
 
1018
1771
  // src/predicate-v2/compile.ts