@sequenceholdings/orm 2.1.0 → 2.1.1

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 (69) hide show
  1. package/README.md +38 -1
  2. package/dist/authoring.cjs +1358 -45
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +223 -2
  5. package/dist/authoring.d.ts +223 -2
  6. package/dist/authoring.js +1349 -47
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +662 -38
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +696 -53
  11. package/dist/canonical.d.ts +696 -53
  12. package/dist/canonical.js +647 -39
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +1808 -247
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +1809 -248
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs +90 -25
  19. package/dist/client.cjs.map +1 -1
  20. package/dist/client.d.cts +39 -22
  21. package/dist/client.d.ts +39 -22
  22. package/dist/client.js +87 -26
  23. package/dist/client.js.map +1 -1
  24. package/dist/compiler.cjs +1325 -285
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +23 -2
  27. package/dist/compiler.d.ts +23 -2
  28. package/dist/compiler.js +1325 -285
  29. package/dist/compiler.js.map +1 -1
  30. package/dist/constants-YoFGjIBM.d.cts +99 -0
  31. package/dist/constants-YoFGjIBM.d.ts +99 -0
  32. package/dist/dynamic-input.d.cts +1 -0
  33. package/dist/dynamic-input.d.ts +1 -0
  34. package/dist/index.cjs +1605 -140
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +230 -113
  37. package/dist/index.d.ts +230 -113
  38. package/dist/index.js +1583 -142
  39. package/dist/index.js.map +1 -1
  40. package/dist/ir.cjs +727 -38
  41. package/dist/ir.cjs.map +1 -1
  42. package/dist/ir.d.cts +539 -21
  43. package/dist/ir.d.ts +539 -21
  44. package/dist/ir.js +722 -39
  45. package/dist/ir.js.map +1 -1
  46. package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
  47. package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
  48. package/dist/policy-1iZbGnZZ.d.cts +24 -0
  49. package/dist/policy-1iZbGnZZ.d.ts +24 -0
  50. package/dist/predicate-v2.cjs +726 -42
  51. package/dist/predicate-v2.cjs.map +1 -1
  52. package/dist/predicate-v2.d.cts +2 -1
  53. package/dist/predicate-v2.d.ts +2 -1
  54. package/dist/predicate-v2.js +726 -42
  55. package/dist/predicate-v2.js.map +1 -1
  56. package/dist/sdl.cjs +858 -54
  57. package/dist/sdl.cjs.map +1 -1
  58. package/dist/sdl.d.cts +7 -3
  59. package/dist/sdl.d.ts +7 -3
  60. package/dist/sdl.js +856 -55
  61. package/dist/sdl.js.map +1 -1
  62. package/package.json +9 -4
  63. package/templates/orm-namespace/CLAUDE.md +6 -4
  64. package/templates/orm-namespace/codegen.preset.d.cts +7 -0
  65. package/templates/orm-namespace/codegen.ts +2 -1
  66. package/templates/orm-namespace/graphql/example/queries.ts +4 -3
  67. package/templates/orm-namespace/package.json +2 -1
  68. package/templates/orm-namespace/schema/tables.ts +3 -2
  69. package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/sdl.cjs CHANGED
@@ -4,14 +4,479 @@ var zod = require('zod');
4
4
  var crypto = require('crypto');
5
5
 
6
6
  // src/canonical/v2.ts
7
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
8
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
9
+
10
+ // src/checks/compile.ts
11
+ function tokenize(source) {
12
+ const tokens = [];
13
+ for (let position = 0; position < source.length; ) {
14
+ const char = source[position];
15
+ if (/\s/.test(char)) {
16
+ position++;
17
+ continue;
18
+ }
19
+ if (char === "(" || char === ")" || char === ",") {
20
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
21
+ position++;
22
+ continue;
23
+ }
24
+ if (char === "'") {
25
+ const start = position++;
26
+ let value = "";
27
+ let closed = false;
28
+ while (position < source.length) {
29
+ if (source[position] !== "'") {
30
+ value += source[position++];
31
+ continue;
32
+ }
33
+ if (source[position + 1] === "'") {
34
+ value += "'";
35
+ position += 2;
36
+ continue;
37
+ }
38
+ position++;
39
+ closed = true;
40
+ break;
41
+ }
42
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
43
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
44
+ tokens.push({ kind: "string", value, position: start });
45
+ continue;
46
+ }
47
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
48
+ if (operator) {
49
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
50
+ position += operator[0].length;
51
+ continue;
52
+ }
53
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
54
+ if (number) {
55
+ tokens.push({ kind: "number", value: number[0], position });
56
+ position += number[0].length;
57
+ continue;
58
+ }
59
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
60
+ if (identifier3) {
61
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
62
+ position += identifier3[0].length;
63
+ continue;
64
+ }
65
+ throw new Error(`unexpected character '${char}' at position ${position}`);
66
+ }
67
+ tokens.push({ kind: "end", value: "", position: source.length });
68
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
69
+ return tokens;
70
+ }
71
+ var Parser = class {
72
+ constructor(tokens, columns) {
73
+ this.tokens = tokens;
74
+ this.columns = /* @__PURE__ */ new Map();
75
+ for (const column of columns) {
76
+ for (const alias of [column.name, column.columnName]) {
77
+ const previous = this.columns.get(alias);
78
+ if (previous && previous.columnName !== column.columnName) {
79
+ throw new Error(`ambiguous check column '${alias}'`);
80
+ }
81
+ this.columns.set(alias, column);
82
+ }
83
+ }
84
+ }
85
+ index = 0;
86
+ depth = 0;
87
+ columns;
88
+ parse() {
89
+ const expression = this.parseOr();
90
+ this.expect("end", "end of expression");
91
+ return expression;
92
+ }
93
+ parseOr() {
94
+ let left = this.parseAnd();
95
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
96
+ return left;
97
+ }
98
+ parseAnd() {
99
+ let left = this.parseNot();
100
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
101
+ return left;
102
+ }
103
+ parseNot() {
104
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
105
+ return this.parsePrimary();
106
+ }
107
+ parsePrimary() {
108
+ if (this.peek().kind === "lparen") {
109
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
110
+ this.advance();
111
+ const expression = this.parseOr();
112
+ this.expect("rparen", "')'");
113
+ this.depth--;
114
+ return `(${expression})`;
115
+ }
116
+ return this.parseComparison();
117
+ }
118
+ parseComparison() {
119
+ const left = this.parseValue();
120
+ if (this.matchKeyword("is")) {
121
+ const negate = this.matchKeyword("not");
122
+ this.expectKeyword("null");
123
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
124
+ }
125
+ if (this.matchKeyword("between")) {
126
+ const lower = this.parseValue();
127
+ this.expectKeyword("and");
128
+ const upper = this.parseValue();
129
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
130
+ }
131
+ if (this.matchKeyword("in")) {
132
+ this.expect("lparen", "'(' after IN");
133
+ const values = [this.parseValue()];
134
+ while (this.peek().kind === "comma") {
135
+ this.advance();
136
+ values.push(this.parseValue());
137
+ }
138
+ this.expect("rparen", "')' after IN values");
139
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
140
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
141
+ }
142
+ const operator = this.expect("operator", "a comparison operator");
143
+ const right = this.parseValue();
144
+ if (operator.value === "~" || operator.value === "!~") {
145
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
146
+ if (!right.literal) throw new Error("regex pattern must be a literal");
147
+ }
148
+ return `${left.sql} ${operator.value} ${right.sql}`;
149
+ }
150
+ parseValue() {
151
+ const token = this.advance();
152
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
153
+ if (token.kind === "number") {
154
+ const number = Number(token.value);
155
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
156
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
157
+ }
158
+ return { sql: token.value, literal: true };
159
+ }
160
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
161
+ const keyword = token.value.toLowerCase();
162
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
163
+ if (keyword === "btrim") {
164
+ this.expect("lparen", "'(' after btrim");
165
+ const column2 = this.parseColumn();
166
+ this.expect("rparen", "')' after btrim column");
167
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
168
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
169
+ }
170
+ const column = this.columns.get(token.value);
171
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
172
+ return { sql: quoteIdentifier(column.columnName), column };
173
+ }
174
+ parseColumn() {
175
+ const token = this.expect("identifier", "a column name");
176
+ const column = this.columns.get(token.value);
177
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
178
+ return column;
179
+ }
180
+ peek() {
181
+ return this.tokens[this.index];
182
+ }
183
+ advance() {
184
+ const token = this.peek();
185
+ if (token.kind !== "end") this.index++;
186
+ return token;
187
+ }
188
+ matchKeyword(keyword) {
189
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
190
+ this.advance();
191
+ return true;
192
+ }
193
+ expectKeyword(keyword) {
194
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
195
+ }
196
+ expect(kind, description) {
197
+ const token = this.peek();
198
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
199
+ return this.advance();
200
+ }
201
+ };
202
+ function compileCheckExpression(source, columns) {
203
+ return new Parser(tokenize(source), columns).parse();
204
+ }
7
205
 
8
206
  // src/constants.ts
9
207
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
208
+ var MAX_IDENTIFIER_LENGTH = 63;
209
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
210
+ var CELL_PART_SUFFIX = {
211
+ at: "__at",
212
+ by: "__by",
213
+ rev: "__rev",
214
+ source: "__src",
215
+ conflicts: "__lost"
216
+ };
217
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
218
+ var CELLS_FIELD = "cells";
219
+ function cellColumnName(column, part) {
220
+ return `${column}${CELL_PART_SUFFIX[part]}`;
221
+ }
222
+ var MATERIALIZE_ACTOR = "orm:materialize";
223
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
224
+ var policyVariantSchema = zod.z.object({
225
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
226
+ when: zod.z.string().min(1).max(1e3),
227
+ /** Exactly one of: allow-all bypass, or a row predicate. */
228
+ allow: zod.z.literal(true).optional(),
229
+ policy: zod.z.string().min(1).max(2e3).optional()
230
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
231
+ message: "A variant declares exactly one of allow: true or policy"
232
+ });
233
+ var policyExpressionSchema = zod.z.union([
234
+ zod.z.string().min(1).max(2e3),
235
+ zod.z.object({
236
+ default: zod.z.string().min(1).max(2e3),
237
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
238
+ })
239
+ ]);
240
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
241
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
242
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
243
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
244
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
245
+ };
246
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
247
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
248
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
249
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
250
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
251
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
252
+ };
253
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
254
+ /* @__PURE__ */ new Set([
255
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
256
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
257
+ ]);
258
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
259
+ var canonicalFunctionRefSchema = zod.z.object({
260
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
261
+ kind: zod.z.enum(["managed", "native"]),
262
+ id: zod.z.string().min(1).max(200)
263
+ }).strict();
264
+ var canonicalFetchShapeSchema = zod.z.union([
265
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
266
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
267
+ ]);
268
+ var canonicalResolvedSourceSchema = zod.z.object({
269
+ name: identifier,
270
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
271
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
272
+ table: identifier,
273
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
274
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
275
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
276
+ updatedAt: zod.z.string().min(1).max(63).optional(),
277
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
278
+ key: zod.z.string().min(1).max(63).optional(),
279
+ fetch: canonicalFetchShapeSchema.optional(),
280
+ function: canonicalFunctionRefSchema.optional(),
281
+ prefetchDefaults: zod.z.object({
282
+ maxStalenessMs: durationMs,
283
+ timeoutMs: durationMs,
284
+ onError: zod.z.enum(["fallback", "fail"])
285
+ }).strict().optional(),
286
+ writeback: zod.z.object({
287
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
288
+ defaultMode: zod.z.enum(["sync", "async"]),
289
+ timeoutMs: durationMs,
290
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
291
+ clearManualOnConfirm: zod.z.boolean()
292
+ }).strict().optional()
293
+ }).strict();
294
+ var canonicalResolveRuleSchema = zod.z.object({
295
+ column: zod.z.string().min(1).max(63),
296
+ comparator: zod.z.enum(COMPARATORS),
297
+ /** Source names, in tie-break order. */
298
+ candidates: zod.z.array(identifier).min(1).max(16),
299
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
300
+ orderBy: zod.z.string().min(1).max(2e3).optional()
301
+ }).strict();
302
+ var canonicalResolvedSchema = zod.z.object({
303
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
304
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
305
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
306
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
307
+ deletedBy: zod.z.array(identifier).max(1).default([]),
308
+ /**
309
+ * Materialized: the view reads an indexed internal table (`table`) that the
310
+ * data plane keeps in step with `compute`, the internal view holding the
311
+ * resolution query. Omitted when the view computes on read.
312
+ */
313
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
314
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
315
+ policies: zod.z.object({
316
+ write: policyExpressionSchema.optional(),
317
+ insert: policyExpressionSchema.optional(),
318
+ update: policyExpressionSchema.optional()
319
+ }).strict().default({})
320
+ }).strict();
321
+ function validateResolvedView({ view, tables, views = [] }) {
322
+ const resolved = view.resolved;
323
+ if (!resolved) return [];
324
+ const errors = [];
325
+ const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
326
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
327
+ if (!pkColumn) {
328
+ fail("requires exactly one primary-key column");
329
+ return errors;
330
+ }
331
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
332
+ const scope = {
333
+ view,
334
+ resolved,
335
+ pkColumn,
336
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
337
+ sourceByName: /* @__PURE__ */ new Map(),
338
+ fail
339
+ };
340
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
341
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
342
+ for (const source of resolved.sources) {
343
+ if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
344
+ scope.sourceByName.set(source.name, source);
345
+ const table = tableByName.get(source.table);
346
+ if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
347
+ else validateSource({ scope, source, table });
348
+ }
349
+ validateRules(scope);
350
+ validateViewCells(scope);
351
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
352
+ for (const name of resolved.deletedBy) {
353
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
354
+ }
355
+ return errors;
356
+ }
357
+ function validateSource({ scope, source, table }) {
358
+ const { fail } = scope;
359
+ if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
360
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
361
+ fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
362
+ }
363
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
364
+ if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
365
+ if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
366
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
367
+ validateSourceColumns({ scope, source, byName });
368
+ if (source.kind === "etl") {
369
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
370
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
371
+ fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
372
+ }
373
+ }
374
+ validatePrefetchFields({ scope, source });
375
+ }
376
+ function validatePrefetchFields({ scope, source }) {
377
+ const { fail, pkColumn } = scope;
378
+ if (source.kind !== "prefetch") {
379
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
380
+ fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
381
+ }
382
+ return;
383
+ }
384
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
385
+ fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
386
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
387
+ fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
388
+ }
389
+ for (const column of source.writeback?.columns ?? []) {
390
+ if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
391
+ }
392
+ }
393
+ function validateSourceColumns({ scope, source, byName }) {
394
+ const { fail, pkColumn, dataColumns } = scope;
395
+ for (const column of [pkColumn.name, ...source.columns]) {
396
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
397
+ const theirs = byName.get(column);
398
+ if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
399
+ else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
400
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
401
+ }
402
+ }
403
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
404
+ const ruled = /* @__PURE__ */ new Set();
405
+ for (const rule of resolved.resolve) {
406
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
407
+ fail(`resolve rule targets unknown column '${rule.column}'`);
408
+ continue;
409
+ }
410
+ if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
411
+ ruled.add(rule.column);
412
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
413
+ fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
414
+ }
415
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
416
+ for (const candidate of rule.candidates) {
417
+ const source = sourceByName.get(candidate);
418
+ if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
419
+ else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
420
+ }
421
+ }
422
+ for (const column of dataColumns.keys()) {
423
+ if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
424
+ }
425
+ }
426
+ var VIEW_CELL_TYPES = {
427
+ source: "text",
428
+ at: "timestamptz",
429
+ by: "text",
430
+ conflicts: "jsonb"
431
+ };
432
+ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
433
+ const expected = /* @__PURE__ */ new Map();
434
+ for (const column of dataColumns.values()) {
435
+ if (column.name === pkColumn.name) continue;
436
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
437
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
438
+ }
439
+ }
440
+ for (const column of view.columns) {
441
+ if (!column.cell) continue;
442
+ const want = expected.get(column.name);
443
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
444
+ fail(`unexpected cell column '${column.name}'`);
445
+ continue;
446
+ }
447
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
448
+ expected.delete(column.name);
449
+ }
450
+ for (const name of expected.keys()) fail(`missing cell column '${name}'`);
451
+ }
452
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
453
+ function validateMaterialized({ scope, tableByName, views }) {
454
+ const { view, resolved, fail } = scope;
455
+ const { table: tableName, compute: computeName } = resolved.materialized;
456
+ const table = tableByName.get(tableName);
457
+ if (!table || table.internal !== true || table.materializes !== view.view) {
458
+ fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
459
+ } else {
460
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
461
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
462
+ }
463
+ const compute = views.find((candidate) => candidate.view === computeName);
464
+ if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
465
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
466
+ for (const source of resolved.sources) {
467
+ if (source.kind !== "etl") continue;
468
+ const read = tableByName.get(source.table)?.policies?.read;
469
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
470
+ if (!unconditional) {
471
+ fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
472
+ }
473
+ }
474
+ }
10
475
 
11
476
  // src/canonical/v2.ts
12
477
  var CANONICAL_FORMAT = 2;
13
478
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
14
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
479
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
15
480
  var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
16
481
  var COLUMN_TYPES = [
17
482
  "uuid",
@@ -27,14 +492,14 @@ var COLUMN_TYPES = [
27
492
  ];
28
493
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
29
494
  var canonicalEnumSchema = zod.z.object({
30
- name: identifier,
495
+ name: identifier2,
31
496
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
32
497
  });
33
498
  var canonicalColumnSchema = zod.z.object({
34
499
  /** Property name in TS (camelCase); columnName is the Postgres name. */
35
500
  name: zod.z.string().min(1).max(63),
36
501
  columnName: columnIdentifier,
37
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
502
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
38
503
  nullable: zod.z.boolean().default(true),
39
504
  unique: zod.z.boolean().default(false),
40
505
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -50,7 +515,14 @@ var canonicalColumnSchema = zod.z.object({
50
515
  /** Migration hints — same semantics as v1. */
51
516
  renamedFrom: columnIdentifier.optional(),
52
517
  cast: zod.z.string().max(200).optional(),
53
- deprecated: zod.z.boolean().default(false)
518
+ deprecated: zod.z.boolean().default(false),
519
+ /**
520
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
521
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
522
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
523
+ * ordinary columns so their hashes are unchanged.
524
+ */
525
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
54
526
  }).superRefine((column, ctx) => {
55
527
  if (column.onUpdateFn === void 0) return;
56
528
  if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
@@ -65,36 +537,26 @@ var canonicalLinkSchema = zod.z.object({
65
537
  name: zod.z.string().min(1).max(63),
66
538
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
67
539
  /** Target table name (namespace-local). Absent for toPlatformUser. */
68
- target: identifier.optional(),
540
+ target: identifier2.optional(),
69
541
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
70
542
  on: columnIdentifier,
71
543
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
72
544
  references: columnIdentifier.optional(),
73
545
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
74
546
  enforce: zod.z.boolean().default(false),
547
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
548
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
75
549
  description: zod.z.string().max(500).optional()
76
550
  });
77
551
  var canonicalIndexSchema = zod.z.object({
78
552
  columns: zod.z.array(columnIdentifier).min(1),
79
553
  unique: zod.z.boolean().default(false),
80
- name: identifier.optional()
81
- });
82
- var policyVariantSchema = zod.z.object({
83
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
84
- when: zod.z.string().min(1).max(1e3),
85
- /** Exactly one of: allow-all bypass, or a row predicate. */
86
- allow: zod.z.literal(true).optional(),
87
- policy: zod.z.string().min(1).max(2e3).optional()
88
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
89
- message: "A variant declares exactly one of allow: true or policy"
554
+ name: identifier2.optional()
90
555
  });
91
- var policyExpressionSchema = zod.z.union([
92
- zod.z.string().min(1).max(2e3),
93
- zod.z.object({
94
- default: zod.z.string().min(1).max(2e3),
95
- variants: zod.z.array(policyVariantSchema).max(10).default([])
96
- })
97
- ]);
556
+ var canonicalCheckSchema = zod.z.object({
557
+ name: identifier2,
558
+ expression: zod.z.string().min(1).max(2e3)
559
+ }).strict();
98
560
  var canonicalPoliciesSchema = zod.z.object({
99
561
  read: policyExpressionSchema.optional(),
100
562
  write: policyExpressionSchema.optional(),
@@ -125,7 +587,7 @@ function isSingleViewQuerySql(value) {
125
587
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
126
588
  }
127
589
  var canonicalActionSchema = zod.z.object({
128
- name: identifier,
590
+ name: identifier2,
129
591
  requires: zod.z.string().max(1e3).optional(),
130
592
  params: zod.z.record(zod.z.string(), zod.z.object({
131
593
  type: columnTypeSchema,
@@ -137,9 +599,9 @@ var canonicalActionSchema = zod.z.object({
137
599
  });
138
600
  var canonicalCowMutationSchema = zod.z.object({
139
601
  kind: zod.z.enum(["override", "revert"]),
140
- name: identifier,
602
+ name: identifier2,
141
603
  /** Synced table (same namespace) the overlay copies from / falls back to. */
142
- synced: identifier,
604
+ synced: identifier2,
143
605
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
144
606
  pk: zod.z.string().min(1).max(63),
145
607
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -167,23 +629,27 @@ var canonicalCowMutationSchema = zod.z.object({
167
629
  }
168
630
  });
169
631
  var canonicalForeignKeySchema = zod.z.object({
170
- name: identifier,
632
+ name: identifier2,
171
633
  columns: zod.z.array(columnIdentifier).min(1).max(8),
172
- target: identifier,
634
+ target: identifier2,
173
635
  references: zod.z.array(columnIdentifier).min(1).max(8),
174
- deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
636
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
637
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
638
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
175
639
  }).strict();
176
640
  var canonicalTableSchema = zod.z.object({
177
- table: identifier,
641
+ table: identifier2,
178
642
  description: zod.z.string().max(2e3).optional(),
179
643
  /** Table-level rename hint (new in v2). */
180
- renamedFrom: identifier.optional(),
644
+ renamedFrom: identifier2.optional(),
181
645
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
182
646
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
183
647
  columns: zod.z.array(canonicalColumnSchema).default([]),
184
648
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
185
649
  links: zod.z.array(canonicalLinkSchema).default([]),
186
650
  indexes: zod.z.array(canonicalIndexSchema).default([]),
651
+ /** Omitted by old definitions, preserving their existing content hashes. */
652
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
187
653
  policies: canonicalPoliciesSchema.default({}),
188
654
  /**
189
655
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -194,22 +660,55 @@ var canonicalTableSchema = zod.z.object({
194
660
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
195
661
  cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
196
662
  /** Omitted by old definitions, preserving their existing content hashes. */
197
- foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
663
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
664
+ /**
665
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
666
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
667
+ * on a resolved table's internal sources), listed explicitly in `columns`
668
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
669
+ * Omitted when off so existing definitions hash unchanged.
670
+ */
671
+ cellMetadata: zod.z.literal(true).optional(),
672
+ /**
673
+ * System-owned backing table of a resolved table (its manual or prefetch
674
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
675
+ * only the data plane writes them. Omitted when false.
676
+ */
677
+ internal: zod.z.literal(true).optional(),
678
+ /**
679
+ * The materialized copy of resolved table `materializes` (internal): the
680
+ * same columns as that view, including its cell parts, maintained by the
681
+ * data plane. Omitted otherwise.
682
+ */
683
+ materializes: identifier2.optional()
198
684
  });
685
+ var MAX_VIEW_QUERY = 1e5;
686
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
199
687
  var canonicalViewSchema = zod.z.object({
200
- view: identifier,
688
+ view: identifier2,
201
689
  description: zod.z.string().max(2e3).optional(),
202
690
  columns: zod.z.array(canonicalColumnSchema).min(1),
203
691
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
204
692
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
205
693
  links: zod.z.array(canonicalLinkSchema).default([]),
206
694
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
207
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
695
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
208
696
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
209
697
  }),
210
698
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
211
- dependencies: zod.z.array(identifier).max(500)
699
+ dependencies: zod.z.array(identifier2).max(500),
700
+ /**
701
+ * Present when this view is a resolved table (defineResolvedTable): the
702
+ * multi-source metadata behind its generated query. Omitted otherwise so
703
+ * existing views hash unchanged.
704
+ */
705
+ resolved: canonicalResolvedSchema.optional(),
706
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
707
+ internal: zod.z.literal(true).optional()
212
708
  }).superRefine((view, ctx) => {
709
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
710
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
711
+ }
213
712
  for (const [index, column] of view.columns.entries()) {
214
713
  if (column.default !== void 0 || column.defaultFn !== void 0) {
215
714
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
@@ -224,15 +723,82 @@ var canonicalViewSchema = zod.z.object({
224
723
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
225
724
  }
226
725
  }
726
+ if (view.resolved === void 0 && view.internal !== true) {
727
+ for (const [index, column] of view.columns.entries()) {
728
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
729
+ }
730
+ }
227
731
  for (const [index, link] of view.links.entries()) {
228
732
  if (link.enforce) {
229
733
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
230
734
  }
735
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
736
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
737
+ }
231
738
  }
232
739
  });
740
+ var CELL_PART_TYPES = {
741
+ at: "timestamptz",
742
+ by: "text",
743
+ rev: "uuid",
744
+ source: "text",
745
+ conflicts: "jsonb"
746
+ };
747
+ function trackedCellColumns(table) {
748
+ const pk = new Set(table.primaryKey ?? []);
749
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
750
+ }
751
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
752
+ function validateCellMetadata(table, index, ctx) {
753
+ const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
754
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
755
+ if (table.materializes !== void 0) return;
756
+ if (table.cellMetadata !== true) {
757
+ for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
758
+ return;
759
+ }
760
+ if (table.source !== void 0) {
761
+ fail(`synced table '${table.table}' cannot track cell metadata`);
762
+ return;
763
+ }
764
+ const tracked = trackedCellColumns(table);
765
+ const byName = new Map(tracked.map((column) => [column.name, column]));
766
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
767
+ if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
768
+ const seen = /* @__PURE__ */ new Set();
769
+ for (const { column, position, cell } of cells) {
770
+ const path = ["tables", index, "columns", position];
771
+ const owner = byName.get(cell.of);
772
+ if (!owner) {
773
+ fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
774
+ continue;
775
+ }
776
+ 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);
777
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
778
+ fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
779
+ }
780
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
781
+ fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
782
+ }
783
+ const key = `${cell.of}:${cell.part}`;
784
+ if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
785
+ seen.add(key);
786
+ }
787
+ for (const column of tracked) {
788
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
789
+ fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
790
+ }
791
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
792
+ fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
793
+ }
794
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
795
+ if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
796
+ }
797
+ }
798
+ }
233
799
  var canonicalDefinitionSchema = zod.z.object({
234
800
  definition_format: zod.z.literal(CANONICAL_FORMAT),
235
- namespace: identifier.max(41),
801
+ namespace: identifier2.max(41),
236
802
  description: zod.z.string().max(2e3).optional(),
237
803
  enums: zod.z.array(canonicalEnumSchema).default([]),
238
804
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
@@ -250,6 +816,7 @@ var canonicalDefinitionSchema = zod.z.object({
250
816
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
251
817
  }
252
818
  seen.add(table.table);
819
+ validateCellMetadata(table, index, ctx);
253
820
  if (table.source !== void 0) {
254
821
  for (const [columnIndex, column] of table.columns.entries()) {
255
822
  if (column.onUpdateFn !== void 0) {
@@ -269,6 +836,26 @@ var canonicalDefinitionSchema = zod.z.object({
269
836
  ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
270
837
  ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
271
838
  ]);
839
+ for (const [index, link] of table.links.entries()) {
840
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
841
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
842
+ }
843
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
844
+ const source = table.columns.find((column) => column.columnName === link.on);
845
+ 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" });
846
+ }
847
+ }
848
+ for (const [index, check] of (table.checks ?? []).entries()) {
849
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
850
+ if (table.source) fail("synced tables cannot declare checks");
851
+ if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
852
+ names.add(check.name);
853
+ try {
854
+ compileCheckExpression(check.expression, table.columns);
855
+ } catch (error) {
856
+ fail(error instanceof Error ? error.message : String(error));
857
+ }
858
+ }
272
859
  for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
273
860
  const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
274
861
  if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
@@ -286,10 +873,18 @@ var canonicalDefinitionSchema = zod.z.object({
286
873
  if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
287
874
  else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
288
875
  }
876
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
877
+ 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");
878
+ }
289
879
  const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
290
880
  if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
291
881
  }
292
882
  }
883
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
884
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
885
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
886
+ }
887
+ }
293
888
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
294
889
  for (const [tableIndex, table] of definition.tables.entries()) {
295
890
  const cowMutations = table.cowMutations ?? [];
@@ -413,8 +1008,12 @@ function canonicalRelations(definition) {
413
1008
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
414
1009
  links: view.links,
415
1010
  indexes: [],
416
- policies: { read: view.policies.read },
417
- actions: []
1011
+ // A resolved table's write policies (evaluated against the resolved row)
1012
+ // ride alongside the view's read policy.
1013
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
1014
+ actions: [],
1015
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
1016
+ ...view.internal ? { internal: true } : {}
418
1017
  }));
419
1018
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
420
1019
  }
@@ -424,13 +1023,14 @@ function normalizeCanonical(definition) {
424
1023
  ...definition,
425
1024
  enums: sortByName(definition.enums),
426
1025
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
427
- const { cowMutations, foreignKeys, ...rest } = t;
1026
+ const { cowMutations, foreignKeys, checks, ...rest } = t;
428
1027
  return {
429
1028
  ...rest,
430
1029
  links: sortByName(t.links),
431
1030
  indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
432
1031
  actions: sortByName(t.actions),
433
1032
  ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1033
+ ...checks?.length ? { checks: sortByName(checks) } : {},
434
1034
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
435
1035
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
436
1036
  };
@@ -536,6 +1136,7 @@ function tableNames(table) {
536
1136
  updateByPkRoot: `update${rootPascal}ByPk`,
537
1137
  deleteRoot: `delete${rootPascal}`,
538
1138
  deleteByPkRoot: `delete${rootPascal}ByPk`,
1139
+ revertByPkRoot: `revert${rootPascal}ByPk`,
539
1140
  boolExp: `${type}BoolExp`,
540
1141
  orderBy: `${type}OrderBy`,
541
1142
  selectColumn: `${type}SelectColumn`,
@@ -561,6 +1162,17 @@ function tableNames(table) {
561
1162
  aggregateGroup: `${type}AggregateGroup`
562
1163
  };
563
1164
  }
1165
+ var CELL_TYPENAMES = {
1166
+ /** A plain table's cell: when and by whom it was last written. */
1167
+ stamp: "CellStamp",
1168
+ /** A resolved table's cell: which source won, when, by whom, and newer losers. */
1169
+ resolved: "ResolvedCell",
1170
+ /** A newer candidate that lost to the column's rule. */
1171
+ conflict: "CellConflict"
1172
+ };
1173
+ function cellsTypename(typename) {
1174
+ return `${typename}Cells`;
1175
+ }
564
1176
  var IR_VERSION = 2;
565
1177
  var LEGACY_IR_VERSION = 1;
566
1178
  var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
@@ -630,6 +1242,21 @@ var selectionIrSchema = zod.z.lazy(
630
1242
  () => zod.z.object({
631
1243
  typename: zod.z.string(),
632
1244
  columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
1245
+ cells: zod.z.object({
1246
+ alias: zod.z.string(),
1247
+ typename: zod.z.string(),
1248
+ fields: zod.z.array(zod.z.object({
1249
+ alias: zod.z.string(),
1250
+ column: zod.z.string(),
1251
+ typename: zod.z.string(),
1252
+ parts: zod.z.array(zod.z.object({
1253
+ alias: zod.z.string(),
1254
+ part: zod.z.enum(["source", "at", "by", "conflicts"]),
1255
+ fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
1256
+ typename: zod.z.string().optional()
1257
+ }))
1258
+ }))
1259
+ }).optional(),
633
1260
  relations: zod.z.array(zod.z.object({
634
1261
  alias: zod.z.string(),
635
1262
  link: zod.z.string(),
@@ -651,6 +1278,11 @@ var selectionIrSchema = zod.z.lazy(
651
1278
  })
652
1279
  );
653
1280
  var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
1281
+ var prefetchPlanIrSchema = zod.z.object({
1282
+ source: zod.z.string(),
1283
+ maxStalenessMs: zod.z.number().int().positive().optional(),
1284
+ key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
1285
+ });
654
1286
  var selectRootSchema = zod.z.object({
655
1287
  kind: zod.z.literal("select"),
656
1288
  alias: zod.z.string(),
@@ -662,7 +1294,8 @@ var selectRootSchema = zod.z.object({
662
1294
  distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
663
1295
  limit: limitIrSchema,
664
1296
  offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
665
- includeIf: includeIfSchema.optional()
1297
+ includeIf: includeIfSchema.optional(),
1298
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
666
1299
  });
667
1300
  var byPkRootSchema = zod.z.object({
668
1301
  kind: zod.z.literal("byPk"),
@@ -672,7 +1305,8 @@ var byPkRootSchema = zod.z.object({
672
1305
  pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
673
1306
  /** Policy predicate — ANDed with the PK match. */
674
1307
  where: predIrSchema,
675
- includeIf: includeIfSchema.optional()
1308
+ includeIf: includeIfSchema.optional(),
1309
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
676
1310
  });
677
1311
  var aggregateRootSchema = zod.z.object({
678
1312
  kind: zod.z.literal("aggregate"),
@@ -730,7 +1364,20 @@ var mutationRootSchema = zod.z.object({
730
1364
  * coalesce view falls back to synced. Both are gated on a matching synced
731
1365
  * row and carry the write policy in `where` like update/delete.
732
1366
  */
733
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
1367
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
1368
+ /**
1369
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
1370
+ * executor gates on the resolved row (write policy in `where`), writes only
1371
+ * the touched cells into the manual source, then re-checks `insertCheck`
1372
+ * against the resolved row. `revert` clears `revertColumns` from the manual
1373
+ * source; `delete` tombstones the row there.
1374
+ */
1375
+ resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
1376
+ revertColumns: zod.z.array(zod.z.string()).optional(),
1377
+ /** `@writeback(source:, mode:)` directives on this root. */
1378
+ writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
1379
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
1380
+ localOnly: zod.z.array(zod.z.string()).optional(),
734
1381
  alias: zod.z.string(),
735
1382
  table: zod.z.string(),
736
1383
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -815,6 +1462,43 @@ var compiledOperationSchema = zod.z.object({
815
1462
  }
816
1463
  }
817
1464
  });
1465
+ var resolvedSourceIrSchema = zod.z.object({
1466
+ name: zod.z.string(),
1467
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
1468
+ /** Logical table key of the backing relation. */
1469
+ table: zod.z.string(),
1470
+ columns: zod.z.array(zod.z.string()),
1471
+ key: zod.z.string().optional(),
1472
+ fetch: zod.z.union([
1473
+ zod.z.object({ shape: zod.z.literal("single") }),
1474
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
1475
+ ]).optional(),
1476
+ function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
1477
+ prefetchDefaults: zod.z.object({
1478
+ maxStalenessMs: zod.z.number().int().positive(),
1479
+ timeoutMs: zod.z.number().int().positive(),
1480
+ onError: zod.z.enum(["fallback", "fail"])
1481
+ }).optional(),
1482
+ writeback: zod.z.object({
1483
+ columns: zod.z.array(zod.z.string()),
1484
+ defaultMode: zod.z.enum(["sync", "async"]),
1485
+ timeoutMs: zod.z.number().int().positive(),
1486
+ clearManualOnConfirm: zod.z.boolean()
1487
+ }).optional()
1488
+ });
1489
+ var resolvedMetaIrSchema = zod.z.object({
1490
+ /** Logical pk column of the resolved view (single-column). */
1491
+ pk: zod.z.string(),
1492
+ /** Source name of the manual source. */
1493
+ manual: zod.z.string(),
1494
+ sources: zod.z.array(resolvedSourceIrSchema),
1495
+ deletedBy: zod.z.array(zod.z.string()),
1496
+ /**
1497
+ * Materialized: the view reads `table`, which the data plane refreshes from
1498
+ * the `compute` view (claims-gated) whenever it writes a source row.
1499
+ */
1500
+ materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
1501
+ });
818
1502
  var tableMetaIrSchema = zod.z.object({
819
1503
  table: zod.z.string(),
820
1504
  schema: zod.z.string(),
@@ -855,6 +1539,22 @@ var tableMetaIrSchema = zod.z.object({
855
1539
  * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
856
1540
  */
857
1541
  touchOnUpdate: zod.z.array(zod.z.string()).optional(),
1542
+ /**
1543
+ * Per-cell metadata: logical column → the logical names of its cell-part
1544
+ * columns (which are listed in `columns`, but never filterable, orderable or
1545
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
1546
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
1547
+ * the relation tracks no cells.
1548
+ */
1549
+ cells: zod.z.record(zod.z.string(), zod.z.object({
1550
+ at: zod.z.string().optional(),
1551
+ by: zod.z.string().optional(),
1552
+ rev: zod.z.string().optional(),
1553
+ source: zod.z.string().optional(),
1554
+ conflicts: zod.z.string().optional()
1555
+ }).strict()).optional(),
1556
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
1557
+ resolved: resolvedMetaIrSchema.optional(),
858
1558
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
859
1559
  /** Depth/node caps for dynamic filter trees on this table. */
860
1560
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
@@ -896,7 +1596,9 @@ var PLATFORM_USER_FIELDS = [
896
1596
  ];
897
1597
  function generateSdl(canonical, options = {}) {
898
1598
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
899
- const tables = canonicalRelations(def).filter((table) => table.source === void 0);
1599
+ const exposed = canonicalRelations(def).filter((table) => table.source === void 0 && table.internal !== true);
1600
+ const cellsByTable = new Map(exposed.map((table) => [table.table, trackedCells(table)]));
1601
+ const tables = exposed.map((table) => ({ ...table, columns: table.columns.filter((column) => column.cell === void 0) }));
900
1602
  if (tables.length === 0) {
901
1603
  throw new SdlGenerationError(
902
1604
  `Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
@@ -955,7 +1657,9 @@ function generateSdl(canonical, options = {}) {
955
1657
  blocks.push(
956
1658
  '"""Abort the entire mutation unless this root affects exactly the document-authored number of rows. Mutation roots only; cannot be conditional."""\ndirective @assertAffectedRows(exactly: Int!) on FIELD'
957
1659
  );
1660
+ if (tables.some((table) => table.resolved !== void 0)) blocks.push(...RESOLVED_DIRECTIVE_BLOCKS);
958
1661
  for (const scalar of CUSTOM_SCALARS) blocks.push(`scalar ${scalar}`);
1662
+ blocks.push(...renderSharedCellTypes([...cellsByTable.values()]));
959
1663
  blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
960
1664
  blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
961
1665
  for (const enumName of [...usedEnums].sort()) {
@@ -980,7 +1684,8 @@ function generateSdl(canonical, options = {}) {
980
1684
  names,
981
1685
  namesByTable,
982
1686
  emitAggregateBoolExp: aggregateBoolExpTargets.has(table.table),
983
- pinned: options.pinnedColumns?.[table.table] ?? []
1687
+ pinned: options.pinnedColumns?.[table.table] ?? [],
1688
+ cells: cellsByTable.get(table.table) ?? null
984
1689
  })
985
1690
  );
986
1691
  }
@@ -1070,6 +1775,52 @@ ${orderFields}
1070
1775
  }`
1071
1776
  ];
1072
1777
  }
1778
+ var RESOLVED_DIRECTIVE_BLOCKS = [
1779
+ '"""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',
1780
+ renderEnum("WritebackMode", ["SYNC", "ASYNC"]),
1781
+ '"""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',
1782
+ '"""Declare that this mutation deliberately keeps writeback columns local to the named source."""\ndirective @localOnly(source: String!) repeatable on FIELD'
1783
+ ];
1784
+ function trackedCells(table) {
1785
+ const columns = [...new Set(table.columns.flatMap((column) => column.cell ? [column.cell.of] : []))];
1786
+ if (columns.length === 0) return null;
1787
+ return { resolved: table.columns.some((column) => column.cell?.part === "source"), columns };
1788
+ }
1789
+ function renderSharedCellTypes(all) {
1790
+ const present = all.filter((cells) => cells !== null);
1791
+ const blocks = [];
1792
+ if (present.some((cells) => !cells.resolved)) {
1793
+ blocks.push(`"""When, and by whom, a cell was last written."""
1794
+ type ${CELL_TYPENAMES.stamp} {
1795
+ at: timestamptz!
1796
+ by: String
1797
+ }`);
1798
+ }
1799
+ if (present.some((cells) => cells.resolved)) {
1800
+ blocks.push(`"""A newer candidate value that lost to the column's resolve rule."""
1801
+ type ${CELL_TYPENAMES.conflict} {
1802
+ source: String!
1803
+ at: timestamptz
1804
+ value: jsonb
1805
+ }`);
1806
+ blocks.push(`"""How a resolved cell was decided: the winning source, its time and actor, and newer losers."""
1807
+ type ${CELL_TYPENAMES.resolved} {
1808
+ source: String!
1809
+ at: timestamptz
1810
+ by: String
1811
+ conflicts: [${CELL_TYPENAMES.conflict}!]!
1812
+ }`);
1813
+ }
1814
+ return blocks;
1815
+ }
1816
+ function renderCellsType(ctx) {
1817
+ if (!ctx.cells) return [];
1818
+ const cellType = ctx.cells.resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp;
1819
+ const lines = ctx.cells.columns.map((column) => ` ${fieldName(column)}: ${cellType}`);
1820
+ return [`type ${cellsTypename(ctx.names.typename)} {
1821
+ ${lines.join("\n")}
1822
+ }`];
1823
+ }
1073
1824
  function relationArgs(target) {
1074
1825
  return `where: ${target.boolExp}, orderBy: [${target.orderBy}!], limit: Int, offset: Int`;
1075
1826
  }
@@ -1078,10 +1829,10 @@ function rootArgs(names) {
1078
1829
  }
1079
1830
  function primaryKeyColumns(table) {
1080
1831
  if (!table.primaryKey || table.primaryKey.length === 0) return [];
1081
- return table.primaryKey.map((identifier2) => {
1082
- const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
1832
+ return table.primaryKey.map((identifier3) => {
1833
+ const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
1083
1834
  if (!column) {
1084
- throw new SdlGenerationError(`Primary key column "${identifier2}" not found on table "${table.table}".`);
1835
+ throw new SdlGenerationError(`Primary key column "${identifier3}" not found on table "${table.table}".`);
1085
1836
  }
1086
1837
  return column;
1087
1838
  });
@@ -1090,6 +1841,7 @@ function renderTableBlocks(ctx) {
1090
1841
  const { table, names } = ctx;
1091
1842
  const blocks = [
1092
1843
  renderObjectType(ctx),
1844
+ ...renderCellsType(ctx),
1093
1845
  renderSelectColumnEnum(ctx),
1094
1846
  renderOrderByInput(ctx),
1095
1847
  renderBoolExp(ctx),
@@ -1104,11 +1856,49 @@ function renderTableBlocks(ctx) {
1104
1856
  ${fields}
1105
1857
  }`);
1106
1858
  }
1107
- if (allowsInsert(table) || allowsUpdate(table)) {
1859
+ if (ctx.table.resolved !== void 0) {
1860
+ blocks.push(...renderResolvedWriteBlocks(ctx));
1861
+ } else if (allowsInsert(table) || allowsUpdate(table)) {
1108
1862
  blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
1109
1863
  }
1110
1864
  return blocks;
1111
1865
  }
1866
+ function resolvedWritableColumns(table) {
1867
+ const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
1868
+ return table.columns.filter((column) => !system.has(column.name));
1869
+ }
1870
+ function renderResolvedWriteBlocks(ctx) {
1871
+ const { table, names } = ctx;
1872
+ const writable = resolvedWritableColumns(table);
1873
+ const pk = new Set(table.primaryKey ?? []);
1874
+ const settable = writable.filter((column) => !pk.has(column.columnName));
1875
+ const line = (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`;
1876
+ const blocks = [];
1877
+ if (allowsInsert(table)) blocks.push(`input ${names.insertInput} {
1878
+ ${writable.map(line).join("\n")}
1879
+ }`);
1880
+ if (allowsUpdate(table)) {
1881
+ blocks.push(`input ${names.setInput} {
1882
+ ${settable.map(line).join("\n")}
1883
+ }`);
1884
+ blocks.push(renderEnum(names.updateColumnEnum, settable.map((column) => fieldName(column.name))));
1885
+ }
1886
+ return blocks;
1887
+ }
1888
+ function renderResolvedMutationFields(table, names) {
1889
+ const lines = [];
1890
+ const pk = primaryKeyColumns(table);
1891
+ if (allowsInsert(table)) lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!): ${names.typename}`);
1892
+ if (allowsUpdate(table) && pk.length > 0) {
1893
+ lines.push(` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, _set: ${names.setInput}!): ${names.typename}`);
1894
+ lines.push(` ${names.revertByPkRoot}(pkColumns: ${names.pkColumnsInput}!, columns: [${names.updateColumnEnum}!]!): ${names.typename}`);
1895
+ if ((table.resolved?.deletedBy.length ?? 0) > 0) {
1896
+ const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
1897
+ lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
1898
+ }
1899
+ }
1900
+ return lines;
1901
+ }
1112
1902
  function allowsInsert(table) {
1113
1903
  return table.policies.write !== void 0 || table.policies.insert !== void 0;
1114
1904
  }
@@ -1131,6 +1921,10 @@ function renderObjectType(ctx) {
1131
1921
  const deprecated = column.deprecated ? " @deprecated" : "";
1132
1922
  lines.push(` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}${bang}${deprecated}`);
1133
1923
  }
1924
+ if (ctx.cells) {
1925
+ lines.push(' """Per-cell metadata: when each field was last written, and by whom."""');
1926
+ lines.push(` ${CELLS_FIELD}: ${cellsTypename(names.typename)}!`);
1927
+ }
1134
1928
  for (const link of table.links) {
1135
1929
  if (link.description !== void 0) lines.push(...descriptionLines(link.description, " "));
1136
1930
  if (link.kind === "toPlatformUser") {
@@ -1199,7 +1993,10 @@ function numericishColumns(table) {
1199
1993
  return table.columns.filter((column) => typeof column.type !== "object" && NUMERICISH.has(column.type));
1200
1994
  }
1201
1995
  function jsonbColumns(table) {
1202
- return table.columns.filter((column) => column.type === "jsonb");
1996
+ return clientWritableColumns(table).filter((column) => column.type === "jsonb");
1997
+ }
1998
+ function clientWritableColumns(table) {
1999
+ return table.columns;
1203
2000
  }
1204
2001
  function renderAggregateTypes(ctx) {
1205
2002
  const { table, names } = ctx;
@@ -1283,14 +2080,14 @@ function constraintValues(table) {
1283
2080
  return [...values].sort();
1284
2081
  }
1285
2082
  function clientSettableColumns(table) {
1286
- return table.columns.filter((column) => column.onUpdateFn === void 0);
2083
+ return clientWritableColumns(table).filter((column) => column.onUpdateFn === void 0);
1287
2084
  }
1288
2085
  function renderWriteBlocks(ctx, access) {
1289
2086
  const { table, names, pinned } = ctx;
1290
2087
  const blocks = [];
1291
2088
  if (access.insert) {
1292
2089
  const pinnedSet = new Set(pinned);
1293
- const insertable = table.columns.filter(
2090
+ const insertable = clientWritableColumns(table).filter(
1294
2091
  (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
1295
2092
  );
1296
2093
  if (insertable.length === 0) {
@@ -1383,6 +2180,10 @@ function renderMutationRoot(tables, namesByTable) {
1383
2180
  if (!canInsert && !canUpdate) continue;
1384
2181
  const names = namesByTable.get(table.table);
1385
2182
  if (!names) continue;
2183
+ if (table.resolved !== void 0) {
2184
+ lines.push(...renderResolvedMutationFields(table, names));
2185
+ continue;
2186
+ }
1386
2187
  const constraints = constraintValues(table);
1387
2188
  const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
1388
2189
  const updateArgs = [`_set: ${names.setInput}`];
@@ -1516,13 +2317,13 @@ function generateTypePolicies(canonical) {
1516
2317
  const entries = [];
1517
2318
  let hasPlatformUser = false;
1518
2319
  for (const table of canonicalRelations(def)) {
1519
- if (table.source !== void 0) continue;
2320
+ if (table.source !== void 0 || table.internal === true) continue;
1520
2321
  if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
1521
2322
  if (!table.primaryKey || table.primaryKey.length === 0) continue;
1522
- const keyFields = table.primaryKey.map((identifier2) => {
1523
- const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
2323
+ const keyFields = table.primaryKey.map((identifier3) => {
2324
+ const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
1524
2325
  if (!column) {
1525
- throw new CodegenGqlError(`Primary key column "${identifier2}" not found on relation "${table.table}".`);
2326
+ throw new CodegenGqlError(`Primary key column "${identifier3}" not found on relation "${table.table}".`);
1526
2327
  }
1527
2328
  return fieldName(column.name);
1528
2329
  });
@@ -1541,6 +2342,7 @@ ${body}
1541
2342
  }
1542
2343
 
1543
2344
  exports.BUCKETABLE_COLUMN_TYPES = BUCKETABLE_COLUMN_TYPES;
2345
+ exports.CELL_TYPENAMES = CELL_TYPENAMES;
1544
2346
  exports.CUSTOM_SCALARS = CUSTOM_SCALARS;
1545
2347
  exports.DATE_BUCKET_ENUM = DATE_BUCKET_ENUM;
1546
2348
  exports.MANIFEST_VERSION = MANIFEST_VERSION;
@@ -1552,6 +2354,7 @@ exports.SdlGenerationError = SdlGenerationError;
1552
2354
  exports.VARIABLE_SCALAR_NAMES = VARIABLE_SCALAR_NAMES;
1553
2355
  exports.buildOperationsManifest = buildOperationsManifest;
1554
2356
  exports.camelCase = camelCase;
2357
+ exports.cellsTypename = cellsTypename;
1555
2358
  exports.columnTypeForScalar = columnTypeForScalar;
1556
2359
  exports.comparisonExpName = comparisonExpName;
1557
2360
  exports.enumTypeName = enumTypeName;
@@ -1563,6 +2366,7 @@ exports.manifestOperationSchema = manifestOperationSchema;
1563
2366
  exports.manifestRequiresSchema = manifestRequiresSchema;
1564
2367
  exports.operationsManifestSchema = operationsManifestSchema;
1565
2368
  exports.pascalCase = pascalCase;
2369
+ exports.resolvedWritableColumns = resolvedWritableColumns;
1566
2370
  exports.tableNames = tableNames;
1567
2371
  exports.typeName = typeName;
1568
2372
  //# sourceMappingURL=sdl.cjs.map