@sequenceholdings/orm 2.1.0 → 2.2.0

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