@sequenceholdings/orm 2.1.0 → 2.2.0

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