@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/cli.cjs CHANGED
@@ -40,12 +40,217 @@ var PredicateParseError = class extends Error {
40
40
  name = "PredicateParseError";
41
41
  };
42
42
 
43
+ // src/compiler/sql.ts
44
+ var CLAIMS_SETTING = "request.jwt.claims";
45
+ var CAPABILITIES_CLAIM_KEY = "capabilities";
46
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
47
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
48
+ var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
49
+ var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
50
+ var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
51
+
52
+ // src/checks/compile.ts
53
+ function tokenize(source) {
54
+ const tokens = [];
55
+ for (let position = 0; position < source.length; ) {
56
+ const char = source[position];
57
+ if (/\s/.test(char)) {
58
+ position++;
59
+ continue;
60
+ }
61
+ if (char === "(" || char === ")" || char === ",") {
62
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
63
+ position++;
64
+ continue;
65
+ }
66
+ if (char === "'") {
67
+ const start = position++;
68
+ let value = "";
69
+ let closed = false;
70
+ while (position < source.length) {
71
+ if (source[position] !== "'") {
72
+ value += source[position++];
73
+ continue;
74
+ }
75
+ if (source[position + 1] === "'") {
76
+ value += "'";
77
+ position += 2;
78
+ continue;
79
+ }
80
+ position++;
81
+ closed = true;
82
+ break;
83
+ }
84
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
85
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
86
+ tokens.push({ kind: "string", value, position: start });
87
+ continue;
88
+ }
89
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
90
+ if (operator) {
91
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
92
+ position += operator[0].length;
93
+ continue;
94
+ }
95
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
96
+ if (number) {
97
+ tokens.push({ kind: "number", value: number[0], position });
98
+ position += number[0].length;
99
+ continue;
100
+ }
101
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
102
+ if (identifier3) {
103
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
104
+ position += identifier3[0].length;
105
+ continue;
106
+ }
107
+ throw new Error(`unexpected character '${char}' at position ${position}`);
108
+ }
109
+ tokens.push({ kind: "end", value: "", position: source.length });
110
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
111
+ return tokens;
112
+ }
113
+ var Parser = class {
114
+ constructor(tokens, columns) {
115
+ this.tokens = tokens;
116
+ this.columns = /* @__PURE__ */ new Map();
117
+ for (const column of columns) {
118
+ for (const alias of [column.name, column.columnName]) {
119
+ const previous = this.columns.get(alias);
120
+ if (previous && previous.columnName !== column.columnName) {
121
+ throw new Error(`ambiguous check column '${alias}'`);
122
+ }
123
+ this.columns.set(alias, column);
124
+ }
125
+ }
126
+ }
127
+ index = 0;
128
+ depth = 0;
129
+ columns;
130
+ parse() {
131
+ const expression = this.parseOr();
132
+ this.expect("end", "end of expression");
133
+ return expression;
134
+ }
135
+ parseOr() {
136
+ let left = this.parseAnd();
137
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
138
+ return left;
139
+ }
140
+ parseAnd() {
141
+ let left = this.parseNot();
142
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
143
+ return left;
144
+ }
145
+ parseNot() {
146
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
147
+ return this.parsePrimary();
148
+ }
149
+ parsePrimary() {
150
+ if (this.peek().kind === "lparen") {
151
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
152
+ this.advance();
153
+ const expression = this.parseOr();
154
+ this.expect("rparen", "')'");
155
+ this.depth--;
156
+ return `(${expression})`;
157
+ }
158
+ return this.parseComparison();
159
+ }
160
+ parseComparison() {
161
+ const left = this.parseValue();
162
+ if (this.matchKeyword("is")) {
163
+ const negate = this.matchKeyword("not");
164
+ this.expectKeyword("null");
165
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
166
+ }
167
+ if (this.matchKeyword("between")) {
168
+ const lower = this.parseValue();
169
+ this.expectKeyword("and");
170
+ const upper = this.parseValue();
171
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
172
+ }
173
+ if (this.matchKeyword("in")) {
174
+ this.expect("lparen", "'(' after IN");
175
+ const values = [this.parseValue()];
176
+ while (this.peek().kind === "comma") {
177
+ this.advance();
178
+ values.push(this.parseValue());
179
+ }
180
+ this.expect("rparen", "')' after IN values");
181
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
182
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
183
+ }
184
+ const operator = this.expect("operator", "a comparison operator");
185
+ const right = this.parseValue();
186
+ if (operator.value === "~" || operator.value === "!~") {
187
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
188
+ if (!right.literal) throw new Error("regex pattern must be a literal");
189
+ }
190
+ return `${left.sql} ${operator.value} ${right.sql}`;
191
+ }
192
+ parseValue() {
193
+ const token = this.advance();
194
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
195
+ if (token.kind === "number") {
196
+ const number = Number(token.value);
197
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
198
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
199
+ }
200
+ return { sql: token.value, literal: true };
201
+ }
202
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
203
+ const keyword = token.value.toLowerCase();
204
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
205
+ if (keyword === "btrim") {
206
+ this.expect("lparen", "'(' after btrim");
207
+ const column2 = this.parseColumn();
208
+ this.expect("rparen", "')' after btrim column");
209
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
210
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
211
+ }
212
+ const column = this.columns.get(token.value);
213
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
214
+ return { sql: quoteIdentifier(column.columnName), column };
215
+ }
216
+ parseColumn() {
217
+ const token = this.expect("identifier", "a column name");
218
+ const column = this.columns.get(token.value);
219
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
220
+ return column;
221
+ }
222
+ peek() {
223
+ return this.tokens[this.index];
224
+ }
225
+ advance() {
226
+ const token = this.peek();
227
+ if (token.kind !== "end") this.index++;
228
+ return token;
229
+ }
230
+ matchKeyword(keyword) {
231
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
232
+ this.advance();
233
+ return true;
234
+ }
235
+ expectKeyword(keyword) {
236
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
237
+ }
238
+ expect(kind, description) {
239
+ const token = this.peek();
240
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
241
+ return this.advance();
242
+ }
243
+ };
244
+ function compileCheckExpression(source, columns) {
245
+ return new Parser(tokenize(source), columns).parse();
246
+ }
247
+
43
248
  // src/constants.ts
44
249
  var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
45
250
  var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
46
251
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
47
252
  var MAX_IDENTIFIER_LENGTH = 63;
48
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
253
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
49
254
  var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
50
255
  var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
51
256
  var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
@@ -61,11 +266,291 @@ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
61
266
  var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
62
267
  var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
63
268
  var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
269
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
270
+ var CELL_PART_SUFFIX = {
271
+ at: "__at",
272
+ by: "__by",
273
+ rev: "__rev",
274
+ source: "__src",
275
+ conflicts: "__lost"
276
+ };
277
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
278
+ var CELLS_FIELD = "cells";
279
+ function cellColumnName(column, part) {
280
+ return `${column}${CELL_PART_SUFFIX[part]}`;
281
+ }
282
+ var MATERIALIZE_ACTOR = "orm:materialize";
283
+ var MATERIALIZE_CLAIMS_JSON = JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
284
+ function materializedRefreshFunctionName(table) {
285
+ return `${table}__refresh`;
286
+ }
287
+ function materializeLockKey(schema, table) {
288
+ return `orm:materialize:${schema}.${table}`;
289
+ }
290
+ var policyVariantSchema = zod.z.object({
291
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
292
+ when: zod.z.string().min(1).max(1e3),
293
+ /** Exactly one of: allow-all bypass, or a row predicate. */
294
+ allow: zod.z.literal(true).optional(),
295
+ policy: zod.z.string().min(1).max(2e3).optional()
296
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
297
+ message: "A variant declares exactly one of allow: true or policy"
298
+ });
299
+ var policyExpressionSchema = zod.z.union([
300
+ zod.z.string().min(1).max(2e3),
301
+ zod.z.object({
302
+ default: zod.z.string().min(1).max(2e3),
303
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
304
+ })
305
+ ]);
306
+ function parseDurationMs(input) {
307
+ if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
308
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
309
+ if (!match) return void 0;
310
+ const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
311
+ const value = Math.round(Number(match[1]) * factor);
312
+ return value > 0 ? value : void 0;
313
+ }
314
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
315
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
316
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
317
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
318
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
319
+ };
320
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
321
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
322
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
323
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
324
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
325
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
326
+ };
327
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
328
+ /* @__PURE__ */ new Set([
329
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
330
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
331
+ ]);
332
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
333
+ var canonicalFunctionRefSchema = zod.z.object({
334
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
335
+ kind: zod.z.enum(["managed", "native"]),
336
+ id: zod.z.string().min(1).max(200)
337
+ }).strict();
338
+ var canonicalFetchShapeSchema = zod.z.union([
339
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
340
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
341
+ ]);
342
+ var canonicalResolvedSourceSchema = zod.z.object({
343
+ name: identifier,
344
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
345
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
346
+ table: identifier,
347
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
348
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
349
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
350
+ updatedAt: zod.z.string().min(1).max(63).optional(),
351
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
352
+ key: zod.z.string().min(1).max(63).optional(),
353
+ fetch: canonicalFetchShapeSchema.optional(),
354
+ function: canonicalFunctionRefSchema.optional(),
355
+ prefetchDefaults: zod.z.object({
356
+ maxStalenessMs: durationMs,
357
+ timeoutMs: durationMs,
358
+ onError: zod.z.enum(["fallback", "fail"])
359
+ }).strict().optional(),
360
+ writeback: zod.z.object({
361
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
362
+ defaultMode: zod.z.enum(["sync", "async"]),
363
+ timeoutMs: durationMs,
364
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
365
+ clearManualOnConfirm: zod.z.boolean()
366
+ }).strict().optional()
367
+ }).strict();
368
+ var canonicalResolveRuleSchema = zod.z.object({
369
+ column: zod.z.string().min(1).max(63),
370
+ comparator: zod.z.enum(COMPARATORS),
371
+ /** Source names, in tie-break order. */
372
+ candidates: zod.z.array(identifier).min(1).max(16),
373
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
374
+ orderBy: zod.z.string().min(1).max(2e3).optional()
375
+ }).strict();
376
+ var canonicalResolvedSchema = zod.z.object({
377
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
378
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
379
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
380
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
381
+ deletedBy: zod.z.array(identifier).max(1).default([]),
382
+ /**
383
+ * Materialized: the view reads an indexed internal table (`table`) that the
384
+ * data plane keeps in step with `compute`, the internal view holding the
385
+ * resolution query. Omitted when the view computes on read.
386
+ */
387
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
388
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
389
+ policies: zod.z.object({
390
+ write: policyExpressionSchema.optional(),
391
+ insert: policyExpressionSchema.optional(),
392
+ update: policyExpressionSchema.optional()
393
+ }).strict().default({})
394
+ }).strict();
395
+ function validateResolvedView({ view, tables, views = [] }) {
396
+ const resolved = view.resolved;
397
+ if (!resolved) return [];
398
+ const errors = [];
399
+ const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
400
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
401
+ if (!pkColumn) {
402
+ fail3("requires exactly one primary-key column");
403
+ return errors;
404
+ }
405
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
406
+ const scope = {
407
+ view,
408
+ resolved,
409
+ pkColumn,
410
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
411
+ sourceByName: /* @__PURE__ */ new Map(),
412
+ fail: fail3
413
+ };
414
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail3("requires exactly one manual source");
415
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
416
+ for (const source of resolved.sources) {
417
+ if (scope.sourceByName.has(source.name)) fail3(`duplicate source '${source.name}'`);
418
+ scope.sourceByName.set(source.name, source);
419
+ const table = tableByName.get(source.table);
420
+ if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
421
+ else validateSource({ scope, source, table });
422
+ }
423
+ validateRules(scope);
424
+ validateViewCells(scope);
425
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
426
+ for (const name of resolved.deletedBy) {
427
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail3(`deletedBy '${name}' must be the manual source`);
428
+ }
429
+ return errors;
430
+ }
431
+ function validateSource({ scope, source, table }) {
432
+ const { fail: fail3 } = scope;
433
+ if (source.kind === "etl" && table.source === void 0) fail3(`etl source '${source.name}' must reference a synced table`);
434
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
435
+ fail3(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
436
+ }
437
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
438
+ if (source.kind === "manual" && !hasRev) fail3(`manual source '${source.name}' must carry '__rev' cell columns`);
439
+ if (source.kind === "prefetch" && hasRev) fail3(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
440
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
441
+ validateSourceColumns({ scope, source, byName });
442
+ if (source.kind === "etl") {
443
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
444
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
445
+ fail3(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
446
+ }
447
+ }
448
+ validatePrefetchFields({ scope, source });
449
+ }
450
+ function validatePrefetchFields({ scope, source }) {
451
+ const { fail: fail3, pkColumn } = scope;
452
+ if (source.kind !== "prefetch") {
453
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
454
+ fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
455
+ }
456
+ return;
457
+ }
458
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
459
+ fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
460
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
461
+ fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
462
+ }
463
+ for (const column of source.writeback?.columns ?? []) {
464
+ if (!source.columns.includes(column)) fail3(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
465
+ }
466
+ }
467
+ function validateSourceColumns({ scope, source, byName }) {
468
+ const { fail: fail3, pkColumn, dataColumns } = scope;
469
+ for (const column of [pkColumn.name, ...source.columns]) {
470
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
471
+ const theirs = byName.get(column);
472
+ if (!own) fail3(`source '${source.name}' supplies unknown column '${column}'`);
473
+ else if (!theirs) fail3(`source '${source.name}' table '${source.table}' has no column '${column}'`);
474
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail3(`column '${column}' has a different type on source '${source.name}'`);
475
+ }
476
+ }
477
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail3 }) {
478
+ const ruled = /* @__PURE__ */ new Set();
479
+ for (const rule of resolved.resolve) {
480
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
481
+ fail3(`resolve rule targets unknown column '${rule.column}'`);
482
+ continue;
483
+ }
484
+ if (ruled.has(rule.column)) fail3(`column '${rule.column}' has more than one resolve rule`);
485
+ ruled.add(rule.column);
486
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
487
+ fail3(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
488
+ }
489
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail3(`column '${rule.column}' repeats a candidate`);
490
+ for (const candidate of rule.candidates) {
491
+ const source = sourceByName.get(candidate);
492
+ if (!source) fail3(`column '${rule.column}' names unknown candidate source '${candidate}'`);
493
+ else if (!source.columns.includes(rule.column)) fail3(`column '${rule.column}' candidate '${candidate}' does not supply it`);
494
+ }
495
+ }
496
+ for (const column of dataColumns.keys()) {
497
+ if (column !== pkColumn.name && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
498
+ }
499
+ }
500
+ var VIEW_CELL_TYPES = {
501
+ source: "text",
502
+ at: "timestamptz",
503
+ by: "text",
504
+ conflicts: "jsonb"
505
+ };
506
+ function validateViewCells({ view, dataColumns, pkColumn, fail: fail3 }) {
507
+ const expected = /* @__PURE__ */ new Map();
508
+ for (const column of dataColumns.values()) {
509
+ if (column.name === pkColumn.name) continue;
510
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
511
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
512
+ }
513
+ }
514
+ for (const column of view.columns) {
515
+ if (!column.cell) continue;
516
+ const want = expected.get(column.name);
517
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
518
+ fail3(`unexpected cell column '${column.name}'`);
519
+ continue;
520
+ }
521
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail3(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
522
+ expected.delete(column.name);
523
+ }
524
+ for (const name of expected.keys()) fail3(`missing cell column '${name}'`);
525
+ }
526
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
527
+ function validateMaterialized({ scope, tableByName, views }) {
528
+ const { view, resolved, fail: fail3 } = scope;
529
+ const { table: tableName, compute: computeName } = resolved.materialized;
530
+ const table = tableByName.get(tableName);
531
+ if (!table || table.internal !== true || table.materializes !== view.view) {
532
+ fail3(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
533
+ } else {
534
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail3(`materialized table '${tableName}' must have exactly the view's columns`);
535
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail3(`materialized table '${tableName}' must share the view's primary key`);
536
+ }
537
+ const compute = views.find((candidate) => candidate.view === computeName);
538
+ if (!compute || compute.internal !== true) fail3(`compute view '${computeName}' must be an internal view`);
539
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail3(`compute view '${computeName}' must have exactly the view's columns`);
540
+ for (const source of resolved.sources) {
541
+ if (source.kind !== "etl") continue;
542
+ const read = tableByName.get(source.table)?.policies?.read;
543
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
544
+ if (!unconditional) {
545
+ fail3(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
546
+ }
547
+ }
548
+ }
64
549
 
65
550
  // src/canonical/v2.ts
66
551
  var CANONICAL_FORMAT = 2;
67
552
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
68
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
553
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
69
554
  var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
70
555
  var COLUMN_TYPES = [
71
556
  "uuid",
@@ -81,14 +566,14 @@ var COLUMN_TYPES = [
81
566
  ];
82
567
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
83
568
  var canonicalEnumSchema = zod.z.object({
84
- name: identifier,
569
+ name: identifier2,
85
570
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
86
571
  });
87
572
  var canonicalColumnSchema = zod.z.object({
88
573
  /** Property name in TS (camelCase); columnName is the Postgres name. */
89
574
  name: zod.z.string().min(1).max(63),
90
575
  columnName: columnIdentifier,
91
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
576
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
92
577
  nullable: zod.z.boolean().default(true),
93
578
  unique: zod.z.boolean().default(false),
94
579
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -104,7 +589,14 @@ var canonicalColumnSchema = zod.z.object({
104
589
  /** Migration hints — same semantics as v1. */
105
590
  renamedFrom: columnIdentifier.optional(),
106
591
  cast: zod.z.string().max(200).optional(),
107
- deprecated: zod.z.boolean().default(false)
592
+ deprecated: zod.z.boolean().default(false),
593
+ /**
594
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
595
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
596
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
597
+ * ordinary columns so their hashes are unchanged.
598
+ */
599
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
108
600
  }).superRefine((column, ctx) => {
109
601
  if (column.onUpdateFn === void 0) return;
110
602
  if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
@@ -119,36 +611,26 @@ var canonicalLinkSchema = zod.z.object({
119
611
  name: zod.z.string().min(1).max(63),
120
612
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
121
613
  /** Target table name (namespace-local). Absent for toPlatformUser. */
122
- target: identifier.optional(),
614
+ target: identifier2.optional(),
123
615
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
124
616
  on: columnIdentifier,
125
617
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
126
618
  references: columnIdentifier.optional(),
127
619
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
128
620
  enforce: zod.z.boolean().default(false),
621
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
622
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
129
623
  description: zod.z.string().max(500).optional()
130
624
  });
131
625
  var canonicalIndexSchema = zod.z.object({
132
626
  columns: zod.z.array(columnIdentifier).min(1),
133
627
  unique: zod.z.boolean().default(false),
134
- name: identifier.optional()
628
+ name: identifier2.optional()
135
629
  });
136
- var policyVariantSchema = zod.z.object({
137
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
138
- when: zod.z.string().min(1).max(1e3),
139
- /** Exactly one of: allow-all bypass, or a row predicate. */
140
- allow: zod.z.literal(true).optional(),
141
- policy: zod.z.string().min(1).max(2e3).optional()
142
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
143
- message: "A variant declares exactly one of allow: true or policy"
144
- });
145
- var policyExpressionSchema = zod.z.union([
146
- zod.z.string().min(1).max(2e3),
147
- zod.z.object({
148
- default: zod.z.string().min(1).max(2e3),
149
- variants: zod.z.array(policyVariantSchema).max(10).default([])
150
- })
151
- ]);
630
+ var canonicalCheckSchema = zod.z.object({
631
+ name: identifier2,
632
+ expression: zod.z.string().min(1).max(2e3)
633
+ }).strict();
152
634
  var canonicalPoliciesSchema = zod.z.object({
153
635
  read: policyExpressionSchema.optional(),
154
636
  write: policyExpressionSchema.optional(),
@@ -336,7 +818,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
336
818
  "width_bucket"
337
819
  ];
338
820
  var canonicalActionSchema = zod.z.object({
339
- name: identifier,
821
+ name: identifier2,
340
822
  requires: zod.z.string().max(1e3).optional(),
341
823
  params: zod.z.record(zod.z.string(), zod.z.object({
342
824
  type: columnTypeSchema,
@@ -348,9 +830,9 @@ var canonicalActionSchema = zod.z.object({
348
830
  });
349
831
  var canonicalCowMutationSchema = zod.z.object({
350
832
  kind: zod.z.enum(["override", "revert"]),
351
- name: identifier,
833
+ name: identifier2,
352
834
  /** Synced table (same namespace) the overlay copies from / falls back to. */
353
- synced: identifier,
835
+ synced: identifier2,
354
836
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
355
837
  pk: zod.z.string().min(1).max(63),
356
838
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -378,23 +860,27 @@ var canonicalCowMutationSchema = zod.z.object({
378
860
  }
379
861
  });
380
862
  var canonicalForeignKeySchema = zod.z.object({
381
- name: identifier,
863
+ name: identifier2,
382
864
  columns: zod.z.array(columnIdentifier).min(1).max(8),
383
- target: identifier,
865
+ target: identifier2,
384
866
  references: zod.z.array(columnIdentifier).min(1).max(8),
385
- deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
867
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
868
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
869
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
386
870
  }).strict();
387
871
  var canonicalTableSchema = zod.z.object({
388
- table: identifier,
872
+ table: identifier2,
389
873
  description: zod.z.string().max(2e3).optional(),
390
874
  /** Table-level rename hint (new in v2). */
391
- renamedFrom: identifier.optional(),
875
+ renamedFrom: identifier2.optional(),
392
876
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
393
877
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
394
878
  columns: zod.z.array(canonicalColumnSchema).default([]),
395
879
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
396
880
  links: zod.z.array(canonicalLinkSchema).default([]),
397
881
  indexes: zod.z.array(canonicalIndexSchema).default([]),
882
+ /** Omitted by old definitions, preserving their existing content hashes. */
883
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
398
884
  policies: canonicalPoliciesSchema.default({}),
399
885
  /**
400
886
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -405,22 +891,55 @@ var canonicalTableSchema = zod.z.object({
405
891
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
406
892
  cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
407
893
  /** Omitted by old definitions, preserving their existing content hashes. */
408
- foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
894
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
895
+ /**
896
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
897
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
898
+ * on a resolved table's internal sources), listed explicitly in `columns`
899
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
900
+ * Omitted when off so existing definitions hash unchanged.
901
+ */
902
+ cellMetadata: zod.z.literal(true).optional(),
903
+ /**
904
+ * System-owned backing table of a resolved table (its manual or prefetch
905
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
906
+ * only the data plane writes them. Omitted when false.
907
+ */
908
+ internal: zod.z.literal(true).optional(),
909
+ /**
910
+ * The materialized copy of resolved table `materializes` (internal): the
911
+ * same columns as that view, including its cell parts, maintained by the
912
+ * data plane. Omitted otherwise.
913
+ */
914
+ materializes: identifier2.optional()
409
915
  });
916
+ var MAX_VIEW_QUERY = 1e5;
917
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
410
918
  var canonicalViewSchema = zod.z.object({
411
- view: identifier,
919
+ view: identifier2,
412
920
  description: zod.z.string().max(2e3).optional(),
413
921
  columns: zod.z.array(canonicalColumnSchema).min(1),
414
922
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
415
923
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
416
924
  links: zod.z.array(canonicalLinkSchema).default([]),
417
925
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
418
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
926
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
419
927
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
420
928
  }),
421
929
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
422
- dependencies: zod.z.array(identifier).max(500)
930
+ dependencies: zod.z.array(identifier2).max(500),
931
+ /**
932
+ * Present when this view is a resolved table (defineResolvedTable): the
933
+ * multi-source metadata behind its generated query. Omitted otherwise so
934
+ * existing views hash unchanged.
935
+ */
936
+ resolved: canonicalResolvedSchema.optional(),
937
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
938
+ internal: zod.z.literal(true).optional()
423
939
  }).superRefine((view, ctx) => {
940
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
941
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
942
+ }
424
943
  for (const [index3, column] of view.columns.entries()) {
425
944
  if (column.default !== void 0 || column.defaultFn !== void 0) {
426
945
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
@@ -435,15 +954,82 @@ var canonicalViewSchema = zod.z.object({
435
954
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
436
955
  }
437
956
  }
957
+ if (view.resolved === void 0 && view.internal !== true) {
958
+ for (const [index3, column] of view.columns.entries()) {
959
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index3], message: `only resolved tables carry cell columns ('${column.name}')` });
960
+ }
961
+ }
438
962
  for (const [index3, link] of view.links.entries()) {
439
963
  if (link.enforce) {
440
964
  ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
441
965
  }
966
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
967
+ ctx.addIssue({ code: "custom", path: ["links", index3], message: `view link '${link.name}' cannot declare referential actions` });
968
+ }
442
969
  }
443
970
  });
971
+ var CELL_PART_TYPES = {
972
+ at: "timestamptz",
973
+ by: "text",
974
+ rev: "uuid",
975
+ source: "text",
976
+ conflicts: "jsonb"
977
+ };
978
+ function trackedCellColumns(table) {
979
+ const pk = new Set(table.primaryKey ?? []);
980
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
981
+ }
982
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
983
+ function validateCellMetadata(table, index3, ctx) {
984
+ const fail3 = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
985
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
986
+ if (table.materializes !== void 0) return;
987
+ if (table.cellMetadata !== true) {
988
+ for (const { position } of cells) fail3(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index3, "columns", position]);
989
+ return;
990
+ }
991
+ if (table.source !== void 0) {
992
+ fail3(`synced table '${table.table}' cannot track cell metadata`);
993
+ return;
994
+ }
995
+ const tracked = trackedCellColumns(table);
996
+ const byName = new Map(tracked.map((column) => [column.name, column]));
997
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
998
+ if (withRev && table.internal !== true) fail3(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
999
+ const seen = /* @__PURE__ */ new Set();
1000
+ for (const { column, position, cell } of cells) {
1001
+ const path = ["tables", index3, "columns", position];
1002
+ const owner = byName.get(cell.of);
1003
+ if (!owner) {
1004
+ fail3(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
1005
+ continue;
1006
+ }
1007
+ if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail3(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
1008
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
1009
+ fail3(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
1010
+ }
1011
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
1012
+ fail3(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
1013
+ }
1014
+ const key = `${cell.of}:${cell.part}`;
1015
+ if (seen.has(key)) fail3(`duplicate cell column '${column.name}'`, path);
1016
+ seen.add(key);
1017
+ }
1018
+ for (const column of tracked) {
1019
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
1020
+ fail3(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
1021
+ }
1022
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
1023
+ fail3(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
1024
+ }
1025
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
1026
+ if (!seen.has(`${column.name}:${part}`)) fail3(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
1027
+ }
1028
+ }
1029
+ }
444
1030
  var canonicalDefinitionSchema = zod.z.object({
445
1031
  definition_format: zod.z.literal(CANONICAL_FORMAT),
446
- namespace: identifier.max(41),
1032
+ namespace: identifier2.max(41),
447
1033
  description: zod.z.string().max(2e3).optional(),
448
1034
  enums: zod.z.array(canonicalEnumSchema).default([]),
449
1035
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
@@ -461,6 +1047,7 @@ var canonicalDefinitionSchema = zod.z.object({
461
1047
  ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
462
1048
  }
463
1049
  seen.add(table.table);
1050
+ validateCellMetadata(table, index3, ctx);
464
1051
  if (table.source !== void 0) {
465
1052
  for (const [columnIndex, column] of table.columns.entries()) {
466
1053
  if (column.onUpdateFn !== void 0) {
@@ -480,25 +1067,53 @@ var canonicalDefinitionSchema = zod.z.object({
480
1067
  ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
481
1068
  ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
482
1069
  ]);
1070
+ for (const [index3, link] of table.links.entries()) {
1071
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
1072
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "referential actions require an enforced toOne link" });
1073
+ }
1074
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
1075
+ const source = table.columns.find((column) => column.columnName === link.on);
1076
+ if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "SET NULL requires a nullable source column" });
1077
+ }
1078
+ }
1079
+ for (const [index3, check] of (table.checks ?? []).entries()) {
1080
+ const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index3], message });
1081
+ if (table.source) fail3("synced tables cannot declare checks");
1082
+ if (names.has(check.name)) fail3(`duplicate constraint '${check.name}'`);
1083
+ names.add(check.name);
1084
+ try {
1085
+ compileCheckExpression(check.expression, table.columns);
1086
+ } catch (error) {
1087
+ fail3(error instanceof Error ? error.message : String(error));
1088
+ }
1089
+ }
483
1090
  for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
484
- const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
485
- if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
1091
+ const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
1092
+ if (names.has(fk.name)) fail3(`duplicate foreign key '${fk.name}'`);
486
1093
  names.add(fk.name);
487
1094
  const target = fkTables.get(fk.target);
488
1095
  if (table.source || !target || target.source) {
489
- fail2("foreign keys must connect owned tables in the same namespace");
1096
+ fail3("foreign keys must connect owned tables in the same namespace");
490
1097
  continue;
491
1098
  }
492
- if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
493
- if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
1099
+ if (fk.columns.length !== fk.references.length) fail3("foreign key columns and references must have the same number of columns");
1100
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail3("foreign key columns must not repeat");
494
1101
  for (const [position, name] of fk.columns.entries()) {
495
1102
  const sourceColumn = table.columns.find((column) => column.columnName === name);
496
1103
  const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
497
- if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
498
- else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
1104
+ if (!sourceColumn || !targetColumn) fail3("foreign key references an unknown column");
1105
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail3("foreign key column types must match");
1106
+ }
1107
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
1108
+ if (fk.columns.some((name) => table.columns.find((column) => column.columnName === name)?.nullable === false || table.primaryKey?.includes(name))) fail3("SET NULL requires nullable source columns");
499
1109
  }
500
1110
  const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
501
- if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail2("foreign key target columns must form a unique key");
1111
+ if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail3("foreign key target columns must form a unique key");
1112
+ }
1113
+ }
1114
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
1115
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
1116
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
502
1117
  }
503
1118
  }
504
1119
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
@@ -624,8 +1239,12 @@ function canonicalRelations(definition) {
624
1239
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
625
1240
  links: view.links,
626
1241
  indexes: [],
627
- policies: { read: view.policies.read },
628
- actions: []
1242
+ // A resolved table's write policies (evaluated against the resolved row)
1243
+ // ride alongside the view's read policy.
1244
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
1245
+ actions: [],
1246
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
1247
+ ...view.internal ? { internal: true } : {}
629
1248
  }));
630
1249
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
631
1250
  }
@@ -635,13 +1254,14 @@ function normalizeCanonical(definition) {
635
1254
  ...definition,
636
1255
  enums: sortByName(definition.enums),
637
1256
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
638
- const { cowMutations, foreignKeys, ...rest } = t2;
1257
+ const { cowMutations, foreignKeys, checks, ...rest } = t2;
639
1258
  return {
640
1259
  ...rest,
641
1260
  links: sortByName(t2.links),
642
1261
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
643
1262
  actions: sortByName(t2.actions),
644
1263
  ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1264
+ ...checks?.length ? { checks: sortByName(checks) } : {},
645
1265
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
646
1266
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
647
1267
  };
@@ -928,8 +1548,19 @@ function isSeqNamespaceConfig(value) {
928
1548
  return typeof value === "object" && value !== null && value.kind === NAMESPACE_CONFIG_BRAND;
929
1549
  }
930
1550
  function serializeNamespace(input) {
931
- const { config, tables } = input;
932
- const views = input.views ?? [];
1551
+ const { config } = input;
1552
+ const views = [...input.views ?? []];
1553
+ const tables = [...input.tables];
1554
+ for (const view of views) {
1555
+ for (const internal of view.config.resolved?.internalTables ?? []) {
1556
+ if (!tables.includes(internal)) tables.push(internal);
1557
+ }
1558
+ }
1559
+ for (const view of [...views]) {
1560
+ for (const internal of view.config.resolved?.internalViews ?? []) {
1561
+ if (!views.includes(internal)) views.push(internal);
1562
+ }
1563
+ }
933
1564
  const enums = input.enums ?? [];
934
1565
  const tablesByName = /* @__PURE__ */ new Map();
935
1566
  for (const table of tables) {
@@ -1031,6 +1662,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1031
1662
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
1032
1663
  if (table.source !== void 0) candidate.source = table.source;
1033
1664
  if (table.source !== void 0) {
1665
+ if (table.config.checks?.length) {
1666
+ throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
1667
+ }
1034
1668
  if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
1035
1669
  throw new OrmDefinitionError(
1036
1670
  `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
@@ -1068,6 +1702,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1068
1702
  if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1069
1703
  candidate.columns?.push(column);
1070
1704
  }
1705
+ if (table.config.internal === true) candidate.internal = true;
1706
+ if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
1707
+ if (table.config.cellMetadata === true && table.source !== void 0) {
1708
+ throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
1709
+ }
1071
1710
  if (table.config.primaryKey !== void 0) {
1072
1711
  if (columnLevelPk.length > 0) {
1073
1712
  throw new OrmDefinitionError(
@@ -1081,6 +1720,24 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1081
1720
  } else if (columnLevelPk.length > 0) {
1082
1721
  candidate.primaryKey = columnLevelPk;
1083
1722
  }
1723
+ if (table.config.cellMetadata === true) {
1724
+ candidate.cellMetadata = true;
1725
+ const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
1726
+ const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
1727
+ for (const column of tracked) {
1728
+ for (const part of parts) {
1729
+ candidate.columns?.push({
1730
+ name: cellColumnName(column.name, part),
1731
+ columnName: cellColumnName(column.columnName, part),
1732
+ type: CELL_PART_TYPES[part],
1733
+ nullable: true,
1734
+ unique: false,
1735
+ deprecated: false,
1736
+ cell: { of: column.name, part }
1737
+ });
1738
+ }
1739
+ }
1740
+ }
1084
1741
  if (table.config.foreignKeys?.length) {
1085
1742
  if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
1086
1743
  candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
@@ -1093,10 +1750,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1093
1750
  columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
1094
1751
  target: target.tableName,
1095
1752
  references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
1096
- ...fk.deferrable ? { deferrable: fk.deferrable } : {}
1753
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {},
1754
+ ...fk.onDelete ? { onDelete: fk.onDelete } : {},
1755
+ ...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
1097
1756
  };
1098
1757
  });
1099
1758
  }
1759
+ if (table.config.checks?.length) {
1760
+ candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
1761
+ }
1100
1762
  for (const index3 of table.config.indexes ?? []) {
1101
1763
  const entry = {
1102
1764
  columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1192,7 +1854,33 @@ function buildCanonicalView({
1192
1854
  columns,
1193
1855
  ...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
1194
1856
  links,
1195
- policies: { read: toCanonicalPolicy(view.config.policies.read) }
1857
+ policies: { read: toCanonicalPolicy(view.config.policies.read) },
1858
+ ...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
1859
+ ...view.config.internal === true ? { internal: true } : {}
1860
+ };
1861
+ }
1862
+ function buildCanonicalResolved(resolved) {
1863
+ const policies = {};
1864
+ if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
1865
+ if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
1866
+ if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
1867
+ return {
1868
+ sources: resolved.sources.map((source) => ({
1869
+ name: source.name,
1870
+ kind: source.kind,
1871
+ table: source.table.tableName,
1872
+ columns: [...source.columns],
1873
+ ...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
1874
+ ...source.key !== void 0 ? { key: source.key } : {},
1875
+ ...source.fetch !== void 0 ? { fetch: source.fetch } : {},
1876
+ ...source.function !== void 0 ? { function: { ...source.function } } : {},
1877
+ ...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
1878
+ ...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
1879
+ })),
1880
+ resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
1881
+ deletedBy: [...resolved.deletedBy],
1882
+ ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
1883
+ policies
1196
1884
  };
1197
1885
  }
1198
1886
  function buildCanonicalLink({
@@ -1203,14 +1891,17 @@ function buildCanonicalLink({
1203
1891
  }) {
1204
1892
  const ownerKind = relationKind(relation);
1205
1893
  const ownerName = relationName(relation);
1206
- const fail2 = (message) => {
1894
+ const fail3 = (message) => {
1207
1895
  throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
1208
1896
  };
1209
1897
  if (link.enforce && link.linkKind !== "toOne") {
1210
- return fail2("sets enforce: true, which is only supported on toOne links");
1898
+ return fail3("sets enforce: true, which is only supported on toOne links");
1899
+ }
1900
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
1901
+ return fail3("sets a referential action without enforce: true");
1211
1902
  }
1212
1903
  if (link.linkKind === "toPlatformUser") {
1213
- if (link.target !== void 0) return fail2("is toPlatformUser and must not declare a target table");
1904
+ if (link.target !== void 0) return fail3("is toPlatformUser and must not declare a target table");
1214
1905
  return {
1215
1906
  name: linkName,
1216
1907
  kind: "toPlatformUser",
@@ -1220,13 +1911,13 @@ function buildCanonicalLink({
1220
1911
  ...link.description !== void 0 ? { description: link.description } : {}
1221
1912
  };
1222
1913
  }
1223
- const target = resolveRelationRef({ ref: link.target, fail: fail2 });
1914
+ const target = resolveRelationRef({ ref: link.target, fail: fail3 });
1224
1915
  const targetName = relationName(target);
1225
1916
  if (!relationsByName.has(targetName)) {
1226
- return fail2(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
1917
+ return fail3(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
1227
1918
  }
1228
1919
  if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
1229
- return fail2("cannot enforce a PostgreSQL foreign key involving a view");
1920
+ return fail3("cannot enforce a PostgreSQL foreign key involving a view");
1230
1921
  }
1231
1922
  const owningSide = link.linkKind === "toOne" ? relation : target;
1232
1923
  const referencedSide = link.linkKind === "toOne" ? target : relation;
@@ -1237,22 +1928,24 @@ function buildCanonicalLink({
1237
1928
  on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1238
1929
  ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
1239
1930
  enforce: link.enforce,
1931
+ ...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
1932
+ ...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
1240
1933
  ...link.description !== void 0 ? { description: link.description } : {}
1241
1934
  };
1242
1935
  }
1243
- function resolveRelationRef({ ref, fail: fail2 }) {
1244
- if (ref === void 0) return fail2("declares no target relation");
1936
+ function resolveRelationRef({ ref, fail: fail3 }) {
1937
+ if (ref === void 0) return fail3("declares no target relation");
1245
1938
  if (typeof ref !== "function") return ref;
1246
1939
  let resolved;
1247
1940
  try {
1248
1941
  resolved = ref();
1249
1942
  } catch (error) {
1250
- return fail2(
1943
+ return fail3(
1251
1944
  `target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
1252
1945
  );
1253
1946
  }
1254
1947
  if (!isSeqTable(resolved) && !isSeqView(resolved)) {
1255
- return fail2("target thunk did not return a relation defined with defineTable/defineView");
1948
+ return fail3("target thunk did not return a relation defined with defineTable/defineView");
1256
1949
  }
1257
1950
  return resolved;
1258
1951
  }
@@ -1552,6 +2245,7 @@ function tableNames(table) {
1552
2245
  updateByPkRoot: `update${rootPascal}ByPk`,
1553
2246
  deleteRoot: `delete${rootPascal}`,
1554
2247
  deleteByPkRoot: `delete${rootPascal}ByPk`,
2248
+ revertByPkRoot: `revert${rootPascal}ByPk`,
1555
2249
  boolExp: `${type}BoolExp`,
1556
2250
  orderBy: `${type}OrderBy`,
1557
2251
  selectColumn: `${type}SelectColumn`,
@@ -1577,6 +2271,17 @@ function tableNames(table) {
1577
2271
  aggregateGroup: `${type}AggregateGroup`
1578
2272
  };
1579
2273
  }
2274
+ var CELL_TYPENAMES = {
2275
+ /** A plain table's cell: when and by whom it was last written. */
2276
+ stamp: "CellStamp",
2277
+ /** A resolved table's cell: which source won, when, by whom, and newer losers. */
2278
+ resolved: "ResolvedCell",
2279
+ /** A newer candidate that lost to the column's rule. */
2280
+ conflict: "CellConflict"
2281
+ };
2282
+ function cellsTypename(typename) {
2283
+ return `${typename}Cells`;
2284
+ }
1580
2285
 
1581
2286
  // src/codegen-gql/type-policies.ts
1582
2287
  function generateTypePolicies(canonical) {
@@ -1584,13 +2289,13 @@ function generateTypePolicies(canonical) {
1584
2289
  const entries = [];
1585
2290
  let hasPlatformUser = false;
1586
2291
  for (const table of canonicalRelations(def)) {
1587
- if (table.source !== void 0) continue;
2292
+ if (table.source !== void 0 || table.internal === true) continue;
1588
2293
  if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
1589
2294
  if (!table.primaryKey || table.primaryKey.length === 0) continue;
1590
- const keyFields = table.primaryKey.map((identifier2) => {
1591
- const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
2295
+ const keyFields = table.primaryKey.map((identifier3) => {
2296
+ const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
1592
2297
  if (!column) {
1593
- throw new CodegenGqlError(`Primary key column "${identifier2}" not found on relation "${table.table}".`);
2298
+ throw new CodegenGqlError(`Primary key column "${identifier3}" not found on relation "${table.table}".`);
1594
2299
  }
1595
2300
  return fieldName(column.name);
1596
2301
  });
@@ -1607,6 +2312,88 @@ ${body}
1607
2312
  }
1608
2313
  `;
1609
2314
  }
2315
+
2316
+ // src/codegen-gql/source-contracts.ts
2317
+ var TS_TYPE = {
2318
+ uuid: "string",
2319
+ text: "string",
2320
+ integer: "number",
2321
+ bigint: "number | string",
2322
+ numeric: "number | string",
2323
+ double: "number | string",
2324
+ boolean: "boolean",
2325
+ timestamptz: "string",
2326
+ date: "string",
2327
+ jsonb: "unknown"
2328
+ };
2329
+ function tsType(column, enums) {
2330
+ if (typeof column.type === "string") return TS_TYPE[column.type] ?? "unknown";
2331
+ const values = enums.get(column.type.enum) ?? [];
2332
+ return values.length > 0 ? values.map((value) => JSON.stringify(value)).join(" | ") : "string";
2333
+ }
2334
+ function generateSourceContracts(canonical) {
2335
+ const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
2336
+ const enums = new Map(def.enums.map((entry) => [entry.name, entry.values]));
2337
+ const blocks = [];
2338
+ for (const view of def.views ?? []) {
2339
+ for (const source of view.resolved?.sources ?? []) {
2340
+ if (source.kind !== "prefetch") continue;
2341
+ const name = `${pascalCase(view.view)}${pascalCase(source.name)}`;
2342
+ const byName = new Map(view.columns.map((column) => [column.name, column]));
2343
+ const key = byName.get(source.key ?? "");
2344
+ const valueLines = source.columns.map((column) => {
2345
+ const def2 = byName.get(column);
2346
+ return ` ${column}?: ${def2 ? tsType(def2, enums) : "unknown"} | null`;
2347
+ });
2348
+ const writable = source.writeback?.columns ?? [];
2349
+ blocks.push([
2350
+ `/** ${view.view} \u2190 ${source.name} (${source.function?.kind ?? "managed"} function '${source.function?.id ?? ""}'). */`,
2351
+ `export type ${name}Key = ${key ? tsType(key, enums) : "string"}`,
2352
+ `export interface ${name}Values {
2353
+ ${valueLines.join("\n")}
2354
+ }`,
2355
+ `export interface ${name}PrefetchRequest {
2356
+ action: 'prefetch'
2357
+ namespace: string
2358
+ table: '${view.view}'
2359
+ source: '${source.name}'
2360
+ keys: ${name}Key[]
2361
+ columns: string[]
2362
+ actor: { sub: string }
2363
+ }`,
2364
+ `export interface ${name}PrefetchResponse {
2365
+ rows: Array<{ key: ${name}Key; values: ${name}Values; cells?: Partial<Record<keyof ${name}Values, string>>; sourceUpdatedAt?: string }>
2366
+ missing?: ${name}Key[]
2367
+ }`,
2368
+ ...writable.length > 0 ? [
2369
+ `export type ${name}WritebackColumn = ${writable.map((column) => JSON.stringify(column)).join(" | ")}`,
2370
+ `export interface ${name}WritebackRequest {
2371
+ action: 'writeback'
2372
+ namespace: string
2373
+ table: '${view.view}'
2374
+ source: '${source.name}'
2375
+ key: ${name}Key
2376
+ changes: Partial<Pick<${name}Values, ${name}WritebackColumn>>
2377
+ before: Partial<Pick<${name}Values, ${name}WritebackColumn>>
2378
+ idempotencyKey: string
2379
+ actor: { sub: string }
2380
+ }`,
2381
+ `/** Respond 200 with this; respond 400/409/422 with { detail } when the source system refuses the change. */
2382
+ export interface ${name}WritebackResponse {
2383
+ values?: ${name}Values
2384
+ cells?: Partial<Record<keyof ${name}Values, string>>
2385
+ sourceUpdatedAt?: string
2386
+ }`
2387
+ ] : []
2388
+ ].join("\n\n"));
2389
+ }
2390
+ }
2391
+ if (blocks.length === 0) return null;
2392
+ return `/* Generated by @sequenceholdings/orm \u2014 resolved-table source function contracts. Do not edit. */
2393
+
2394
+ ${blocks.join("\n\n")}
2395
+ `;
2396
+ }
1610
2397
  var IR_VERSION = 2;
1611
2398
  var LEGACY_IR_VERSION = 1;
1612
2399
  var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
@@ -1690,6 +2477,21 @@ var selectionIrSchema = zod.z.lazy(
1690
2477
  () => zod.z.object({
1691
2478
  typename: zod.z.string(),
1692
2479
  columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
2480
+ cells: zod.z.object({
2481
+ alias: zod.z.string(),
2482
+ typename: zod.z.string(),
2483
+ fields: zod.z.array(zod.z.object({
2484
+ alias: zod.z.string(),
2485
+ column: zod.z.string(),
2486
+ typename: zod.z.string(),
2487
+ parts: zod.z.array(zod.z.object({
2488
+ alias: zod.z.string(),
2489
+ part: zod.z.enum(["source", "at", "by", "conflicts"]),
2490
+ fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
2491
+ typename: zod.z.string().optional()
2492
+ }))
2493
+ }))
2494
+ }).optional(),
1693
2495
  relations: zod.z.array(zod.z.object({
1694
2496
  alias: zod.z.string(),
1695
2497
  link: zod.z.string(),
@@ -1711,6 +2513,11 @@ var selectionIrSchema = zod.z.lazy(
1711
2513
  })
1712
2514
  );
1713
2515
  var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
2516
+ var prefetchPlanIrSchema = zod.z.object({
2517
+ source: zod.z.string(),
2518
+ maxStalenessMs: zod.z.number().int().positive().optional(),
2519
+ key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
2520
+ });
1714
2521
  var selectRootSchema = zod.z.object({
1715
2522
  kind: zod.z.literal("select"),
1716
2523
  alias: zod.z.string(),
@@ -1722,7 +2529,8 @@ var selectRootSchema = zod.z.object({
1722
2529
  distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
1723
2530
  limit: limitIrSchema,
1724
2531
  offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
1725
- includeIf: includeIfSchema.optional()
2532
+ includeIf: includeIfSchema.optional(),
2533
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
1726
2534
  });
1727
2535
  var byPkRootSchema = zod.z.object({
1728
2536
  kind: zod.z.literal("byPk"),
@@ -1732,7 +2540,8 @@ var byPkRootSchema = zod.z.object({
1732
2540
  pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
1733
2541
  /** Policy predicate — ANDed with the PK match. */
1734
2542
  where: predIrSchema,
1735
- includeIf: includeIfSchema.optional()
2543
+ includeIf: includeIfSchema.optional(),
2544
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
1736
2545
  });
1737
2546
  var aggregateRootSchema = zod.z.object({
1738
2547
  kind: zod.z.literal("aggregate"),
@@ -1790,7 +2599,20 @@ var mutationRootSchema = zod.z.object({
1790
2599
  * coalesce view falls back to synced. Both are gated on a matching synced
1791
2600
  * row and carry the write policy in `where` like update/delete.
1792
2601
  */
1793
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
2602
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
2603
+ /**
2604
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
2605
+ * executor gates on the resolved row (write policy in `where`), writes only
2606
+ * the touched cells into the manual source, then re-checks `insertCheck`
2607
+ * against the resolved row. `revert` clears `revertColumns` from the manual
2608
+ * source; `delete` tombstones the row there.
2609
+ */
2610
+ resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
2611
+ revertColumns: zod.z.array(zod.z.string()).optional(),
2612
+ /** `@writeback(source:, mode:)` directives on this root. */
2613
+ writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
2614
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
2615
+ localOnly: zod.z.array(zod.z.string()).optional(),
1794
2616
  alias: zod.z.string(),
1795
2617
  table: zod.z.string(),
1796
2618
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -1875,6 +2697,43 @@ var compiledOperationSchema = zod.z.object({
1875
2697
  }
1876
2698
  }
1877
2699
  });
2700
+ var resolvedSourceIrSchema = zod.z.object({
2701
+ name: zod.z.string(),
2702
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
2703
+ /** Logical table key of the backing relation. */
2704
+ table: zod.z.string(),
2705
+ columns: zod.z.array(zod.z.string()),
2706
+ key: zod.z.string().optional(),
2707
+ fetch: zod.z.union([
2708
+ zod.z.object({ shape: zod.z.literal("single") }),
2709
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
2710
+ ]).optional(),
2711
+ function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
2712
+ prefetchDefaults: zod.z.object({
2713
+ maxStalenessMs: zod.z.number().int().positive(),
2714
+ timeoutMs: zod.z.number().int().positive(),
2715
+ onError: zod.z.enum(["fallback", "fail"])
2716
+ }).optional(),
2717
+ writeback: zod.z.object({
2718
+ columns: zod.z.array(zod.z.string()),
2719
+ defaultMode: zod.z.enum(["sync", "async"]),
2720
+ timeoutMs: zod.z.number().int().positive(),
2721
+ clearManualOnConfirm: zod.z.boolean()
2722
+ }).optional()
2723
+ });
2724
+ var resolvedMetaIrSchema = zod.z.object({
2725
+ /** Logical pk column of the resolved view (single-column). */
2726
+ pk: zod.z.string(),
2727
+ /** Source name of the manual source. */
2728
+ manual: zod.z.string(),
2729
+ sources: zod.z.array(resolvedSourceIrSchema),
2730
+ deletedBy: zod.z.array(zod.z.string()),
2731
+ /**
2732
+ * Materialized: the view reads `table`, which the data plane refreshes from
2733
+ * the `compute` view (claims-gated) whenever it writes a source row.
2734
+ */
2735
+ materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
2736
+ });
1878
2737
  var tableMetaIrSchema = zod.z.object({
1879
2738
  table: zod.z.string(),
1880
2739
  schema: zod.z.string(),
@@ -1915,6 +2774,22 @@ var tableMetaIrSchema = zod.z.object({
1915
2774
  * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
1916
2775
  */
1917
2776
  touchOnUpdate: zod.z.array(zod.z.string()).optional(),
2777
+ /**
2778
+ * Per-cell metadata: logical column → the logical names of its cell-part
2779
+ * columns (which are listed in `columns`, but never filterable, orderable or
2780
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
2781
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
2782
+ * the relation tracks no cells.
2783
+ */
2784
+ cells: zod.z.record(zod.z.string(), zod.z.object({
2785
+ at: zod.z.string().optional(),
2786
+ by: zod.z.string().optional(),
2787
+ rev: zod.z.string().optional(),
2788
+ source: zod.z.string().optional(),
2789
+ conflicts: zod.z.string().optional()
2790
+ }).strict()).optional(),
2791
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
2792
+ resolved: resolvedMetaIrSchema.optional(),
1918
2793
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
1919
2794
  /** Depth/node caps for dynamic filter trees on this table. */
1920
2795
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
@@ -1983,7 +2858,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
1983
2858
  var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
1984
2859
  var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
1985
2860
  var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
1986
- function tokenize(source) {
2861
+ function tokenize2(source) {
1987
2862
  const tokens = [];
1988
2863
  let position = 0;
1989
2864
  while (position < source.length) {
@@ -2062,7 +2937,7 @@ function readString(source, start) {
2062
2937
  }
2063
2938
  throw new PolicyParseError("unterminated string literal", start);
2064
2939
  }
2065
- var Parser = class {
2940
+ var Parser2 = class {
2066
2941
  constructor(tokens) {
2067
2942
  this.tokens = tokens;
2068
2943
  }
@@ -2236,7 +3111,7 @@ var Parser = class {
2236
3111
  function parsePolicy(source) {
2237
3112
  const trimmed = source.trim();
2238
3113
  if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
2239
- return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
3114
+ return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
2240
3115
  }
2241
3116
 
2242
3117
  // src/predicate-v2/compile.ts
@@ -2628,6 +3503,7 @@ var PolicyInjector = class {
2628
3503
  injectMutation(root) {
2629
3504
  const table = root.table;
2630
3505
  const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
3506
+ if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
2631
3507
  if (root.op === "insert" || root.op === "insertOne") {
2632
3508
  const insertPolicy = this.insert(table);
2633
3509
  if (predContainsExists(insertPolicy)) {
@@ -2669,6 +3545,24 @@ var PolicyInjector = class {
2669
3545
  }
2670
3546
  return { ...root, where, returning };
2671
3547
  }
3548
+ /**
3549
+ * Resolved writes are policed against the RESOLVED row, not the sparse
3550
+ * manual row: `where` gates the existing row (update/revert/delete) and
3551
+ * `insertCheck` is re-evaluated as a SELECT over the view after the write,
3552
+ * so link-traversing (exists) policies are representable here.
3553
+ */
3554
+ injectResolvedWrite(root, returning) {
3555
+ const table = root.table;
3556
+ if (root.resolvedAction === "insert") {
3557
+ const pins = (this.ctx.pins.get(table) ?? []).map(
3558
+ (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
3559
+ );
3560
+ return { ...root, pinnedColumns: pins, insertCheck: this.insert(table), returning };
3561
+ }
3562
+ const updatePolicy = this.update(table);
3563
+ const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
3564
+ return root.resolvedAction === "delete" ? { ...root, where, returning } : { ...root, where, insertCheck: updatePolicy, returning };
3565
+ }
2672
3566
  };
2673
3567
  function collectTableSet(roots, ctx) {
2674
3568
  const tables = /* @__PURE__ */ new Set();
@@ -2728,6 +3622,7 @@ function collectTableSet(roots, ctx) {
2728
3622
  break;
2729
3623
  case "mutation":
2730
3624
  if (root.cowSynced !== void 0) tables.add(root.cowSynced);
3625
+ for (const source of ctx.tables[root.table]?.resolved?.sources ?? []) tables.add(source.table);
2731
3626
  if (root.where) visitPred(root.where, root.table);
2732
3627
  if (root.insertCheck) visitPred(root.insertCheck, root.table);
2733
3628
  if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
@@ -2849,7 +3744,8 @@ function resolveColumnEntries(table, enums, diagnostics) {
2849
3744
  type,
2850
3745
  nullable: column.nullable,
2851
3746
  deprecated: column.deprecated,
2852
- onUpdateFn: column.onUpdateFn !== void 0
3747
+ onUpdateFn: column.onUpdateFn !== void 0,
3748
+ ...column.cell ? { cell: column.cell } : {}
2853
3749
  };
2854
3750
  });
2855
3751
  }
@@ -2886,8 +3782,8 @@ function buildCompileContext(rawCanonical) {
2886
3782
  const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
2887
3783
  const primaryKey2 = [];
2888
3784
  for (const ref of pkRefs) {
2889
- const resolved = logicalColumn(entries, ref);
2890
- if (!resolved) {
3785
+ const resolved2 = logicalColumn(entries, ref);
3786
+ if (!resolved2) {
2891
3787
  diagnostics.push({
2892
3788
  severity: "error",
2893
3789
  code: "unknown-pk-column",
@@ -2896,9 +3792,12 @@ function buildCompileContext(rawCanonical) {
2896
3792
  });
2897
3793
  continue;
2898
3794
  }
2899
- primaryKey2.push(resolved);
3795
+ primaryKey2.push(resolved2);
2900
3796
  }
2901
- const active = entries.filter((c) => !c.deprecated);
3797
+ const resolved = isView ? table.resolved : void 0;
3798
+ const data = entries.filter((c) => c.cell === void 0);
3799
+ const cells = cellMap(entries);
3800
+ const active = data.filter((c) => !c.deprecated);
2902
3801
  const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
2903
3802
  const constraints = [];
2904
3803
  if (primaryKey2.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey2] });
@@ -2923,12 +3822,14 @@ function buildCompileContext(rawCanonical) {
2923
3822
  columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
2924
3823
  primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
2925
3824
  links: [],
2926
- filterable: entries.map((c) => c.name),
2927
- orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
2928
- insertable: isView ? [] : active.map((c) => c.name),
2929
- settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
3825
+ filterable: data.map((c) => c.name),
3826
+ orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
3827
+ insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
3828
+ settable: resolved ? resolvedWritable(data).filter((name) => !primaryKey2.includes(name)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
2930
3829
  appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
2931
3830
  ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
3831
+ ...cells ? { cells } : {},
3832
+ ...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
2932
3833
  constraints: isView ? [] : constraints,
2933
3834
  dynamicFilterMaxDepth: 6,
2934
3835
  dynamicFilterMaxNodes: 64
@@ -3001,7 +3902,7 @@ function buildCompileContext(rawCanonical) {
3001
3902
  compilePolicyFor("write", writePolicies);
3002
3903
  compilePolicyFor("insert", insertPolicies);
3003
3904
  compilePolicyFor("update", updatePolicies);
3004
- if (isView) {
3905
+ if (isView && table.resolved === void 0) {
3005
3906
  writePolicies.set(table.table, CONST_FALSE);
3006
3907
  insertPolicies.set(table.table, CONST_FALSE);
3007
3908
  updatePolicies.set(table.table, CONST_FALSE);
@@ -3040,6 +3941,7 @@ function buildCompileContext(rawCanonical) {
3040
3941
  const queryRoots = /* @__PURE__ */ new Map();
3041
3942
  const mutationRoots = /* @__PURE__ */ new Map();
3042
3943
  for (const table of relations) {
3944
+ if (table.internal === true) continue;
3043
3945
  const roots = queryRootNames(table.table);
3044
3946
  queryRoots.set(roots.select, { kind: "select", table: table.table });
3045
3947
  if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
@@ -3055,6 +3957,7 @@ function buildCompileContext(rawCanonical) {
3055
3957
  mutationRoots.set(field, { op, table: table.table });
3056
3958
  }
3057
3959
  }
3960
+ if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
3058
3961
  }
3059
3962
  for (const table of relations) {
3060
3963
  for (const mutation of table.cowMutations ?? []) {
@@ -3125,6 +4028,49 @@ function buildCompileContext(rawCanonical) {
3125
4028
  diagnostics
3126
4029
  };
3127
4030
  }
4031
+ function registerResolvedRoots({ table, mutationRoots }) {
4032
+ const names = tableNames(table.table);
4033
+ const canInsert = table.policies.write !== void 0 || table.policies.insert !== void 0;
4034
+ const canUpdate = table.policies.write !== void 0 || table.policies.update !== void 0;
4035
+ const add = (field, action) => mutationRoots.set(field, { op: "resolvedWrite", action, table: table.table });
4036
+ if (canInsert) add(names.insertOneRoot, "insert");
4037
+ if (canUpdate) {
4038
+ add(names.updateByPkRoot, "update");
4039
+ add(names.revertByPkRoot, "revert");
4040
+ if ((table.resolved?.deletedBy.length ?? 0) > 0) add(names.deleteByPkRoot, "delete");
4041
+ }
4042
+ }
4043
+ function cellMap(entries) {
4044
+ const cells = {};
4045
+ for (const entry of entries) {
4046
+ if (entry.cell) cells[entry.cell.of] = { ...cells[entry.cell.of], [entry.cell.part]: entry.name };
4047
+ }
4048
+ return Object.keys(cells).length > 0 ? cells : void 0;
4049
+ }
4050
+ function resolvedWritable(entries) {
4051
+ const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
4052
+ return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
4053
+ }
4054
+ function resolvedMeta({ resolved, pk }) {
4055
+ const manual = resolved.sources.find((source) => source.kind === "manual");
4056
+ return {
4057
+ pk,
4058
+ manual: manual?.name ?? "",
4059
+ deletedBy: [...resolved.deletedBy],
4060
+ ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
4061
+ sources: resolved.sources.map((source) => ({
4062
+ name: source.name,
4063
+ kind: source.kind,
4064
+ table: source.table,
4065
+ columns: [...source.columns],
4066
+ ...source.key !== void 0 ? { key: source.key } : {},
4067
+ ...source.fetch !== void 0 ? { fetch: source.fetch } : {},
4068
+ ...source.function !== void 0 ? { function: source.function } : {},
4069
+ ...source.prefetchDefaults !== void 0 ? { prefetchDefaults: source.prefetchDefaults } : {},
4070
+ ...source.writeback !== void 0 ? { writeback: source.writeback } : {}
4071
+ }))
4072
+ };
4073
+ }
3128
4074
  function flattenSelectionSet(selectionSet, fragments, stack) {
3129
4075
  const selections = [];
3130
4076
  for (const selection of selectionSet.selections) {
@@ -3182,6 +4128,136 @@ function computeOpsHash(opHashes) {
3182
4128
  const digest = crypto.createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
3183
4129
  return `sha256:${digest}`;
3184
4130
  }
4131
+ var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "localOnly"]);
4132
+ var fail = (message, code) => {
4133
+ throw new DocumentCompileError(message, code);
4134
+ };
4135
+ function readArgs(node, name) {
4136
+ return (node.directives ?? []).filter((directive) => directive.name.value === name).map((directive) => {
4137
+ const args = {};
4138
+ for (const arg of directive.arguments ?? []) {
4139
+ const key = arg.name.value;
4140
+ if (key === "source" || key === "maxStaleness") {
4141
+ if (arg.value.kind !== graphql.Kind.STRING) fail(`@${name}(${key}:) must be a string literal`, "invalid-source-directive");
4142
+ args[key] = arg.value.value;
4143
+ } else if (key === "mode" && name === "writeback") {
4144
+ if (arg.value.kind !== graphql.Kind.ENUM || !["SYNC", "ASYNC"].includes(arg.value.value)) {
4145
+ fail("@writeback(mode:) must be SYNC or ASYNC", "invalid-source-directive");
4146
+ }
4147
+ args.mode = arg.value.value === "SYNC" ? "sync" : "async";
4148
+ } else {
4149
+ fail(`@${name} does not take argument '${key}'`, "invalid-source-directive");
4150
+ }
4151
+ }
4152
+ if (!args.source) fail(`@${name} needs a source: argument`, "invalid-source-directive");
4153
+ return args;
4154
+ });
4155
+ }
4156
+ function prefetchSourceFor({ resolved, name, directive, table }) {
4157
+ if (!resolved) return fail(`@${directive} is only valid on resolved tables \u2014 '${table}' is not one`, "invalid-source-directive");
4158
+ const source = resolved.sources.find((entry) => entry.name === name);
4159
+ if (!source || source.kind !== "prefetch") {
4160
+ const options = resolved.sources.filter((entry) => entry.kind === "prefetch").map((entry) => entry.name);
4161
+ return fail(`@${directive}(source: "${name}") on '${table}' must name a prefetch source (${options.join(", ") || "none declared"})`, "invalid-source-directive");
4162
+ }
4163
+ return source;
4164
+ }
4165
+ function applyPrefetchDirectives({ node, root, meta }) {
4166
+ const directives = readArgs(node, "prefetch");
4167
+ for (const name of ["writeback", "localOnly"]) {
4168
+ if (readArgs(node, name).length > 0) fail(`@${name} is only valid on resolved-table mutation roots`, "invalid-source-directive");
4169
+ }
4170
+ if (directives.length === 0) return root;
4171
+ if (root.kind !== "select" && root.kind !== "byPk") {
4172
+ return fail(`@prefetch is only valid on select and byPk roots, not '${root.alias}'`, "invalid-source-directive");
4173
+ }
4174
+ const seen = /* @__PURE__ */ new Set();
4175
+ const plans = directives.map((directive) => {
4176
+ const source = prefetchSourceFor({ resolved: meta.resolved, name: directive.source, directive: "prefetch", table: root.table });
4177
+ if (seen.has(source.name)) fail(`@prefetch(source: "${source.name}") appears twice on '${root.alias}'`, "invalid-source-directive");
4178
+ seen.add(source.name);
4179
+ const maxStalenessMs = directive.maxStaleness === void 0 ? void 0 : parseDurationMs(directive.maxStaleness);
4180
+ if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
4181
+ fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
4182
+ }
4183
+ const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk: meta.resolved.pk, source, alias: root.alias });
4184
+ return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
4185
+ });
4186
+ return { ...root, prefetch: plans };
4187
+ }
4188
+ function selectKey({ where, pk, source, alias }) {
4189
+ const leaves = where.kind === "and" ? where.operands : [where];
4190
+ const pinned = leaves.find((leaf) => leaf.kind === "cmp" && leaf.column === pk && (leaf.op === "eq" || leaf.op === "in"));
4191
+ const shape = source.fetch?.shape ?? "single";
4192
+ if (!pinned || pinned.kind !== "cmp") {
4193
+ return fail(
4194
+ `@prefetch(source: "${source.name}") on '${alias}': the root is not key-addressed \u2014 its where must pin '${pk}' with _eq${shape === "batch" ? " or _in" : ""} (${source.name} supports ${shape === "batch" ? "batch" : "single-key"} fetch)`,
4195
+ "prefetch-shape-mismatch"
4196
+ );
4197
+ }
4198
+ if (pinned.op === "in" && shape !== "batch") {
4199
+ return fail(`@prefetch(source: "${source.name}") on '${alias}': ${source.name} only supports single-key fetch, but the root selects '${pk}' with _in`, "prefetch-shape-mismatch");
4200
+ }
4201
+ return pinned.op === "eq" ? { one: pinned.value } : { many: pinned.value };
4202
+ }
4203
+ function applyWritebackDirectives({ node, root, meta }) {
4204
+ if (readArgs(node, "prefetch").length > 0) fail("@prefetch is only valid on query roots", "invalid-source-directive");
4205
+ const writebacks = readArgs(node, "writeback");
4206
+ const localOnly = readArgs(node, "localOnly");
4207
+ if (root.kind !== "mutation" || root.op !== "resolvedWrite" || !meta.resolved) {
4208
+ if (writebacks.length + localOnly.length > 0) fail("@writeback/@localOnly are only valid on resolved-table mutation roots", "invalid-source-directive");
4209
+ return root;
4210
+ }
4211
+ const resolved = meta.resolved;
4212
+ const declared = /* @__PURE__ */ new Map();
4213
+ const bindings = [];
4214
+ for (const directive of writebacks) {
4215
+ const source = prefetchSourceFor({ resolved, name: directive.source, directive: "writeback", table: root.table });
4216
+ if (!source.writeback) fail(`@writeback(source: "${source.name}"): source '${source.name}' declares no writeback columns`, "invalid-source-directive");
4217
+ if (root.resolvedAction !== "update") {
4218
+ fail(`@writeback on '${root.alias}': only update roots write back in v1 (use @localOnly for ${root.resolvedAction ?? "this"} roots)`, "writeback-shape-mismatch");
4219
+ }
4220
+ if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
4221
+ declared.set(source.name, "writeback");
4222
+ bindings.push({ source: source.name, mode: directive.mode ?? source.writeback.defaultMode });
4223
+ }
4224
+ for (const directive of localOnly) {
4225
+ const source = prefetchSourceFor({ resolved, name: directive.source, directive: "localOnly", table: root.table });
4226
+ if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
4227
+ declared.set(source.name, "localOnly");
4228
+ }
4229
+ assertWritebacksDeclared({ root, meta, resolved, declared });
4230
+ return {
4231
+ ...root,
4232
+ ...bindings.length > 0 ? { writeback: bindings } : {},
4233
+ ...localOnly.length > 0 ? { localOnly: localOnly.map((directive) => directive.source) } : {}
4234
+ };
4235
+ }
4236
+ function assertWritebacksDeclared({ root, meta, resolved, declared }) {
4237
+ const written = writtenColumns({ root, meta });
4238
+ for (const source of resolved.sources) {
4239
+ const columns = source.writeback?.columns.filter((column) => written.has(column)) ?? [];
4240
+ if (columns.length > 0 && !declared.has(source.name)) {
4241
+ fail(
4242
+ `'${root.alias}' sets ${columns.map((column) => `'${column}'`).join(", ")}, which source '${source.name}' writes back \u2014 add @writeback(source: "${source.name}") to push the change or @localOnly(source: "${source.name}") to keep it local`,
4243
+ "writeback-undeclared"
4244
+ );
4245
+ }
4246
+ }
4247
+ }
4248
+ function writtenColumns({ root, meta }) {
4249
+ if (root.resolvedAction === "update") {
4250
+ if (!root.set || "slot" in root.set) return new Set(meta.settable);
4251
+ return new Set(root.set.entries.map((entry) => entry.column));
4252
+ }
4253
+ if (root.resolvedAction === "insert") {
4254
+ if (!root.objects || "slot" in root.objects) return new Set(meta.insertable);
4255
+ return new Set(root.objects.rows.flatMap((row) => row.map((entry) => entry.column)));
4256
+ }
4257
+ return /* @__PURE__ */ new Set();
4258
+ }
4259
+
4260
+ // src/compiler-gql/lower.ts
3185
4261
  var DEFAULT_LIMIT = 100;
3186
4262
  var MAX_LIMIT = 1e3;
3187
4263
  var MAX_GROUPS = 1e3;
@@ -3219,7 +4295,7 @@ var ORDER_DIRECTIONS = {
3219
4295
  DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
3220
4296
  };
3221
4297
  var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
3222
- function fail(message, code, candidates = []) {
4298
+ function fail2(message, code, candidates = []) {
3223
4299
  throw new DocumentCompileError(message, code, candidates);
3224
4300
  }
3225
4301
  function argsByName(field) {
@@ -3227,16 +4303,16 @@ function argsByName(field) {
3227
4303
  }
3228
4304
  function* aggregateBlockFields(selectionSet) {
3229
4305
  for (const node of selectionSet.selections) {
3230
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
4306
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3231
4307
  if (node.name.value === "__typename") continue;
3232
- if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
4308
+ if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
3233
4309
  yield node;
3234
4310
  }
3235
4311
  }
3236
4312
  function assertKnownArgs(field, allowed) {
3237
4313
  for (const arg of field.arguments ?? []) {
3238
4314
  if (!allowed.includes(arg.name.value)) {
3239
- fail(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
4315
+ fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
3240
4316
  }
3241
4317
  }
3242
4318
  }
@@ -3249,8 +4325,11 @@ var OperationLowerer = class {
3249
4325
  meta(table) {
3250
4326
  return this.ctx.tables[table];
3251
4327
  }
4328
+ /** A data column — cell-metadata columns are reachable only through `cells { … }`. */
3252
4329
  column(table, name) {
3253
- return this.meta(table).columns.find((c) => c.name === name);
4330
+ const meta = this.meta(table);
4331
+ const column = meta.columns.find((c) => c.name === name);
4332
+ return column && !isCellColumn(meta, name) ? column : void 0;
3254
4333
  }
3255
4334
  link(table, name) {
3256
4335
  return this.meta(table).links.find((l) => l.name === name);
@@ -3258,7 +4337,8 @@ var OperationLowerer = class {
3258
4337
  fieldCandidates(table) {
3259
4338
  const meta = this.meta(table);
3260
4339
  return [
3261
- ...meta.columns.map((c) => c.name),
4340
+ ...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
4341
+ ...meta.cells ? [CELLS_FIELD] : [],
3262
4342
  ...meta.links.map((l) => l.name),
3263
4343
  ...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
3264
4344
  ];
@@ -3267,7 +4347,7 @@ var OperationLowerer = class {
3267
4347
  slot(name, input, table) {
3268
4348
  const variable = this.variables.get(name);
3269
4349
  if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
3270
- fail(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
4350
+ fail2(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
3271
4351
  }
3272
4352
  return name;
3273
4353
  }
@@ -3292,14 +4372,14 @@ var OperationLowerer = class {
3292
4372
  const items = node.values.map((item) => {
3293
4373
  const lowered = this.lowerValue(item);
3294
4374
  if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
3295
- fail("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
4375
+ fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
3296
4376
  }
3297
4377
  return lowered.value;
3298
4378
  });
3299
4379
  return { kind: "literal", value: items };
3300
4380
  }
3301
4381
  case graphql.Kind.OBJECT:
3302
- fail("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
4382
+ fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
3303
4383
  }
3304
4384
  }
3305
4385
  // -------------------------------------------------------------------------
@@ -3309,7 +4389,7 @@ var OperationLowerer = class {
3309
4389
  if (node.kind === graphql.Kind.VARIABLE) {
3310
4390
  return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
3311
4391
  }
3312
- if (node.kind !== graphql.Kind.OBJECT) fail(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
4392
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
3313
4393
  const operands = [];
3314
4394
  for (const field of node.fields) {
3315
4395
  const key = field.name.value;
@@ -3340,45 +4420,45 @@ var OperationLowerer = class {
3340
4420
  continue;
3341
4421
  }
3342
4422
  }
3343
- fail(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
4423
+ fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
3344
4424
  }
3345
4425
  if (operands.length === 0) return CONST_TRUE;
3346
4426
  return operands.length === 1 ? operands[0] : { kind: "and", operands };
3347
4427
  }
3348
4428
  lowerComparison(node, column, table) {
3349
4429
  if (node.kind !== graphql.Kind.OBJECT) {
3350
- fail(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
4430
+ fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
3351
4431
  }
3352
4432
  return node.fields.map((field) => {
3353
4433
  const op = BOOLEXP_OPS[field.name.value];
3354
4434
  if (!op) {
3355
- fail(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
4435
+ fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
3356
4436
  }
3357
4437
  return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
3358
4438
  });
3359
4439
  }
3360
4440
  /** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
3361
4441
  lowerAggPredicate(node, link, target) {
3362
- if (node.kind !== graphql.Kind.OBJECT) fail(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
4442
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
3363
4443
  const countField = node.fields.find((f) => f.name.value === "count");
3364
4444
  if (!countField || node.fields.length !== 1) {
3365
- fail(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
4445
+ fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
3366
4446
  }
3367
- if (countField.value.kind !== graphql.Kind.OBJECT) fail(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
4447
+ if (countField.value.kind !== graphql.Kind.OBJECT) fail2(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
3368
4448
  let predicate;
3369
4449
  let where;
3370
4450
  for (const field of countField.value.fields) {
3371
4451
  if (field.name.value === "predicate") {
3372
4452
  if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
3373
- fail(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
4453
+ fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
3374
4454
  }
3375
4455
  const opField = field.value.fields[0];
3376
4456
  const opName = opField.name.value;
3377
4457
  if (!AGG_PREDICATE_OPS.includes(opName)) {
3378
- fail(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
4458
+ fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
3379
4459
  }
3380
4460
  if (opField.value.kind !== graphql.Kind.INT) {
3381
- fail(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
4461
+ fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
3382
4462
  }
3383
4463
  predicate = { op: opName.slice(1), value: Number(opField.value.value) };
3384
4464
  continue;
@@ -3387,9 +4467,9 @@ var OperationLowerer = class {
3387
4467
  where = this.lowerBoolExp(field.value, target);
3388
4468
  continue;
3389
4469
  }
3390
- fail(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
4470
+ fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
3391
4471
  }
3392
- if (!predicate) fail(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
4472
+ if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
3393
4473
  return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
3394
4474
  }
3395
4475
  lowerOrderBy(node, table) {
@@ -3399,7 +4479,7 @@ var OperationLowerer = class {
3399
4479
  const items = node.kind === graphql.Kind.LIST ? node.values : [node];
3400
4480
  const lowered = [];
3401
4481
  for (const item of items) {
3402
- if (item.kind !== graphql.Kind.OBJECT) fail(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
4482
+ if (item.kind !== graphql.Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
3403
4483
  for (const field of item.fields) {
3404
4484
  const key = field.name.value;
3405
4485
  const column = this.column(table, key);
@@ -3410,25 +4490,25 @@ var OperationLowerer = class {
3410
4490
  const link = this.link(table, key);
3411
4491
  if (link && link.kind !== "toMany") {
3412
4492
  if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
3413
- fail(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
4493
+ fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
3414
4494
  }
3415
4495
  const inner = field.value.fields[0];
3416
4496
  const targetColumn = this.column(link.target, inner.name.value);
3417
4497
  if (!targetColumn) {
3418
- fail(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
4498
+ fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
3419
4499
  }
3420
4500
  lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
3421
4501
  continue;
3422
4502
  }
3423
- fail(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
4503
+ fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
3424
4504
  }
3425
4505
  }
3426
4506
  return lowered;
3427
4507
  }
3428
4508
  lowerDirection(node, table, column) {
3429
- if (node.kind !== graphql.Kind.ENUM) fail(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
4509
+ if (node.kind !== graphql.Kind.ENUM) fail2(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
3430
4510
  const direction = ORDER_DIRECTIONS[node.value];
3431
- if (!direction) fail(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
4511
+ if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
3432
4512
  return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
3433
4513
  }
3434
4514
  lowerDistinctOn(node, table) {
@@ -3437,9 +4517,9 @@ var OperationLowerer = class {
3437
4517
  }
3438
4518
  const items = node.kind === graphql.Kind.LIST ? node.values : [node];
3439
4519
  return items.map((item) => {
3440
- if (item.kind !== graphql.Kind.ENUM) fail(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
4520
+ if (item.kind !== graphql.Kind.ENUM) fail2(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
3441
4521
  if (!this.column(table, item.value)) {
3442
- fail(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
4522
+ fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3443
4523
  }
3444
4524
  return item.value;
3445
4525
  });
@@ -3450,16 +4530,16 @@ var OperationLowerer = class {
3450
4530
  if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
3451
4531
  if (node.kind === graphql.Kind.INT) {
3452
4532
  const value = Number(node.value);
3453
- if (value < 1) fail("limit must be a positive integer", "invalid-limit");
4533
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
3454
4534
  return { ...base, value: Math.min(value, MAX_LIMIT) };
3455
4535
  }
3456
- fail("limit must be an integer literal or a variable", "invalid-limit");
4536
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
3457
4537
  }
3458
4538
  lowerOffset(node) {
3459
4539
  if (!node) return void 0;
3460
4540
  if (node.kind === graphql.Kind.VARIABLE) return { variable: node.name.value };
3461
4541
  if (node.kind === graphql.Kind.INT) return { value: Number(node.value) };
3462
- fail("offset must be an integer literal or a variable", "invalid-offset");
4542
+ fail2("offset must be an integer literal or a variable", "invalid-offset");
3463
4543
  }
3464
4544
  // -------------------------------------------------------------------------
3465
4545
  // Selections
@@ -3468,9 +4548,9 @@ var OperationLowerer = class {
3468
4548
  const meta = this.meta(table);
3469
4549
  const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
3470
4550
  for (const node of selectionSet.selections) {
3471
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
4551
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3472
4552
  if (node.directives && node.directives.length > 0) {
3473
- fail(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
4553
+ fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
3474
4554
  }
3475
4555
  const name = node.name.value;
3476
4556
  if (name === "__typename") continue;
@@ -3478,14 +4558,19 @@ var OperationLowerer = class {
3478
4558
  const column = this.column(table, name);
3479
4559
  if (column) {
3480
4560
  if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
3481
- fail(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
4561
+ fail2(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
3482
4562
  }
3483
4563
  selection.columns.push({ alias, column: name });
3484
4564
  continue;
3485
4565
  }
4566
+ if (name === CELLS_FIELD && meta.cells) {
4567
+ if (selection.cells) fail2(`'${CELLS_FIELD}' may be selected once per object`, "invalid-selection");
4568
+ selection.cells = this.lowerCells(node, table, alias);
4569
+ continue;
4570
+ }
3486
4571
  const link = this.link(table, name);
3487
4572
  if (link) {
3488
- if (!node.selectionSet) fail(`link '${table}.${name}' needs a selection set`, "invalid-selection");
4573
+ if (!node.selectionSet) fail2(`link '${table}.${name}' needs a selection set`, "invalid-selection");
3489
4574
  const relation = {
3490
4575
  alias,
3491
4576
  link: name,
@@ -3504,7 +4589,7 @@ var OperationLowerer = class {
3504
4589
  if (offset) relation.offset = offset;
3505
4590
  }
3506
4591
  } else if (node.arguments && node.arguments.length > 0) {
3507
- fail(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
4592
+ fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
3508
4593
  }
3509
4594
  selection.relations.push(relation);
3510
4595
  continue;
@@ -3512,7 +4597,7 @@ var OperationLowerer = class {
3512
4597
  if (name.endsWith("Aggregate")) {
3513
4598
  const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
3514
4599
  if (aggLink && aggLink.kind === "toMany") {
3515
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4600
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3516
4601
  assertKnownArgs(node, ["where"]);
3517
4602
  const whereArg = argsByName(node).get("where");
3518
4603
  const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
@@ -3525,10 +4610,57 @@ var OperationLowerer = class {
3525
4610
  continue;
3526
4611
  }
3527
4612
  }
3528
- fail(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
4613
+ fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
3529
4614
  }
3530
4615
  return selection;
3531
4616
  }
4617
+ /**
4618
+ * `cells { <column> { at by … } }` — typed per-cell metadata. Plain tables
4619
+ * offer `at`/`by`; resolved tables add the winning `source` and newer
4620
+ * `conflicts { source at value }`.
4621
+ */
4622
+ lowerCells(node, table, alias) {
4623
+ const meta = this.meta(table);
4624
+ const cells = meta.cells ?? {};
4625
+ if (node.arguments && node.arguments.length > 0) fail2(`'${CELLS_FIELD}' takes no arguments`, "invalid-selection");
4626
+ if (!node.selectionSet) fail2(`'${CELLS_FIELD}' needs a selection set`, "invalid-selection");
4627
+ const fields = [];
4628
+ for (const cellNode of fieldNodes(node.selectionSet)) {
4629
+ const column = cellNode.name.value;
4630
+ const parts = cells[column];
4631
+ if (!parts || parts.at === void 0 && parts.source === void 0) {
4632
+ fail2(`unknown cell '${column}' on '${table}'`, "unknown-field", Object.keys(cells));
4633
+ }
4634
+ if (!cellNode.selectionSet) fail2(`cell '${column}' needs a selection set`, "invalid-selection");
4635
+ const resolved = parts.source !== void 0;
4636
+ const allowed = resolved ? ["source", "at", "by", "conflicts"] : ["at", "by"];
4637
+ const selected = [];
4638
+ for (const partNode of fieldNodes(cellNode.selectionSet)) {
4639
+ const part = partNode.name.value;
4640
+ if (!allowed.includes(part)) fail2(`unknown field '${part}' on cell '${column}'`, "unknown-field", allowed);
4641
+ const partAlias = partNode.alias?.value ?? part;
4642
+ if (part !== "conflicts") {
4643
+ if (partNode.selectionSet) fail2(`cell field '${part}' takes no sub-selection`, "invalid-selection");
4644
+ selected.push({ alias: partAlias, part });
4645
+ continue;
4646
+ }
4647
+ if (!partNode.selectionSet) fail2(`'conflicts' needs a selection set`, "invalid-selection");
4648
+ const conflictFields = fieldNodes(partNode.selectionSet).map((fieldNode) => {
4649
+ const field = fieldNode.name.value;
4650
+ if (field !== "source" && field !== "at" && field !== "value") fail2(`unknown field '${field}' on a cell conflict`, "unknown-field", ["source", "at", "value"]);
4651
+ return { alias: fieldNode.alias?.value ?? field, field };
4652
+ });
4653
+ selected.push({ alias: partAlias, part: "conflicts", fields: conflictFields, typename: CELL_TYPENAMES.conflict });
4654
+ }
4655
+ fields.push({
4656
+ alias: cellNode.alias?.value ?? column,
4657
+ column,
4658
+ typename: resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp,
4659
+ parts: selected
4660
+ });
4661
+ }
4662
+ return { alias, typename: cellsTypename(meta.typename), fields };
4663
+ }
3532
4664
  /** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
3533
4665
  lowerAggregateSelection(selectionSet, table) {
3534
4666
  const result = {};
@@ -3536,17 +4668,17 @@ var OperationLowerer = class {
3536
4668
  for (const node of aggregateBlockFields(selectionSet)) {
3537
4669
  const name = node.name.value;
3538
4670
  if (name === "nodes") {
3539
- if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
4671
+ if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
3540
4672
  result.nodes = this.lowerSelection(node.selectionSet, table);
3541
4673
  continue;
3542
4674
  }
3543
- if (name !== "aggregate") fail(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
3544
- if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
4675
+ if (name !== "aggregate") fail2(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
4676
+ if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3545
4677
  sawAggregate = true;
3546
- if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
4678
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
3547
4679
  Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
3548
4680
  }
3549
- if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
4681
+ if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
3550
4682
  return result;
3551
4683
  }
3552
4684
  /**
@@ -3563,24 +4695,24 @@ var OperationLowerer = class {
3563
4695
  for (const node of aggregateBlockFields(selectionSet)) {
3564
4696
  const name = node.name.value;
3565
4697
  if (name === "key") {
3566
- if (sawKey) fail("one key block per grouped aggregate", "invalid-aggregate");
4698
+ if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
3567
4699
  sawKey = true;
3568
- if (!node.selectionSet) fail("'key' needs a selection set", "invalid-aggregate");
4700
+ if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
3569
4701
  result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
3570
4702
  continue;
3571
4703
  }
3572
4704
  if (name === "nodes") {
3573
- fail("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
4705
+ fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
3574
4706
  }
3575
- if (name !== "aggregate") fail(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
3576
- if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
4707
+ if (name !== "aggregate") fail2(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
4708
+ if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3577
4709
  sawAggregate = true;
3578
- if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
4710
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
3579
4711
  Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
3580
4712
  }
3581
4713
  const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
3582
4714
  if (result.key.length === 0 && !rendersAggregate) {
3583
- fail(
4715
+ fail2(
3584
4716
  "grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
3585
4717
  "invalid-aggregate",
3586
4718
  ["key", "aggregate"]
@@ -3597,14 +4729,14 @@ var OperationLowerer = class {
3597
4729
  lowerGroupKeySelection(selectionSet, table, groupColumns) {
3598
4730
  const key = [];
3599
4731
  for (const node of selectionSet.selections) {
3600
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
4732
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3601
4733
  const name = node.name.value;
3602
4734
  if (name === "__typename") continue;
3603
4735
  if (!this.column(table, name)) {
3604
- fail(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
4736
+ fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
3605
4737
  }
3606
4738
  if (!groupColumns.has(name)) {
3607
- fail(
4739
+ fail2(
3608
4740
  `key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
3609
4741
  "invalid-group-key",
3610
4742
  [...groupColumns]
@@ -3627,21 +4759,21 @@ var OperationLowerer = class {
3627
4759
  if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
3628
4760
  const distinct = args.get("distinct");
3629
4761
  if (distinct) {
3630
- if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
4762
+ if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
3631
4763
  count.distinct = distinct.value.value;
3632
4764
  }
3633
4765
  result.count = count;
3634
4766
  continue;
3635
4767
  }
3636
4768
  if (aggName === "sum" || aggName === "avg") {
3637
- if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
4769
+ if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
3638
4770
  const columns = [];
3639
4771
  for (const columnField of aggField.selectionSet.selections) {
3640
- if (columnField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
4772
+ if (columnField.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3641
4773
  if (columnField.name.value === "__typename") continue;
3642
4774
  const column = this.column(table, columnField.name.value);
3643
4775
  if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
3644
- fail(
4776
+ fail2(
3645
4777
  `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
3646
4778
  "invalid-aggregate",
3647
4779
  this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
@@ -3652,7 +4784,7 @@ var OperationLowerer = class {
3652
4784
  result[aggName] = columns;
3653
4785
  continue;
3654
4786
  }
3655
- fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
4787
+ fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
3656
4788
  }
3657
4789
  return result;
3658
4790
  }
@@ -3662,41 +4794,43 @@ var OperationLowerer = class {
3662
4794
  lowerRoots(operation, kind) {
3663
4795
  const roots = [];
3664
4796
  for (const node of operation.selectionSet.selections) {
3665
- if (node.kind !== graphql.Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
4797
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
3666
4798
  if (node.selectionSet) graphql.visit(node.selectionSet, {
3667
4799
  Directive(directive) {
3668
4800
  if (directive.name.value === "assertAffectedRows") {
3669
- fail("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
4801
+ fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
3670
4802
  }
3671
4803
  }
3672
4804
  });
3673
4805
  const assertion = this.parseAffectedRowAssertion(node, kind);
3674
4806
  const gate = this.parseRootDirectives(node);
3675
4807
  if (gate.omit) continue;
3676
- const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
4808
+ const lowered = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
4809
+ const meta = this.meta(lowered.table);
4810
+ const root = kind === "query" ? applyPrefetchDirectives({ node, root: lowered, meta }) : applyWritebackDirectives({ node, root: lowered, meta });
3677
4811
  const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
3678
4812
  roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
3679
4813
  }
3680
- if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
4814
+ if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
3681
4815
  return roots;
3682
4816
  }
3683
4817
  parseAffectedRowAssertion(node, kind) {
3684
4818
  const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
3685
4819
  if (!assertions.length) return void 0;
3686
4820
  if (kind !== "mutation" || assertions.length !== 1) {
3687
- fail("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
4821
+ fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
3688
4822
  }
3689
4823
  if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
3690
- fail("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
4824
+ fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
3691
4825
  }
3692
4826
  const args = assertions[0].arguments;
3693
4827
  const arg = args?.[0];
3694
4828
  if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
3695
- fail("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
4829
+ fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
3696
4830
  }
3697
4831
  const count = Number(arg.value.value);
3698
4832
  if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
3699
- fail("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
4833
+ fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
3700
4834
  }
3701
4835
  return count;
3702
4836
  }
@@ -3706,18 +4840,19 @@ var OperationLowerer = class {
3706
4840
  for (const directive of node.directives ?? []) {
3707
4841
  const name = directive.name.value;
3708
4842
  if (name === "assertAffectedRows") continue;
4843
+ if (SOURCE_DIRECTIVES.has(name)) continue;
3709
4844
  if (name !== "include" && name !== "skip") {
3710
- fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
4845
+ fail2(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
3711
4846
  }
3712
4847
  const ifArg = directive.arguments?.find((a) => a.name.value === "if");
3713
- if (!ifArg) fail(`@${name} needs an if: argument`, "invalid-directive");
4848
+ if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
3714
4849
  if (ifArg.value.kind === graphql.Kind.BOOLEAN) {
3715
4850
  const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
3716
4851
  if (!keep) omit = true;
3717
4852
  continue;
3718
4853
  }
3719
- if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
3720
- if (includeIf) fail("at most one of @include/@skip per root field", "invalid-directive");
4854
+ if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail2(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
4855
+ if (includeIf) fail2("at most one of @include/@skip per root field", "invalid-directive");
3721
4856
  includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
3722
4857
  }
3723
4858
  return omit ? { omit: true } : { includeIf, omit: false };
@@ -3726,12 +4861,12 @@ var OperationLowerer = class {
3726
4861
  const name = node.name.value;
3727
4862
  const alias = node.alias?.value ?? name;
3728
4863
  const rootInfo = this.ctx.queryRoots.get(name);
3729
- if (!rootInfo) fail(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
4864
+ if (!rootInfo) fail2(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
3730
4865
  const { table } = rootInfo;
3731
4866
  const args = argsByName(node);
3732
4867
  if (rootInfo.kind === "select") {
3733
4868
  assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
3734
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4869
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3735
4870
  const where = args.get("where");
3736
4871
  const orderBy = args.get("orderBy");
3737
4872
  const distinctOn = args.get("distinctOn");
@@ -3750,7 +4885,7 @@ var OperationLowerer = class {
3750
4885
  };
3751
4886
  }
3752
4887
  if (rootInfo.kind === "byPk") {
3753
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4888
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3754
4889
  const pk = this.lowerPkArgs(node, table);
3755
4890
  return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
3756
4891
  }
@@ -3758,7 +4893,7 @@ var OperationLowerer = class {
3758
4893
  return this.lowerAggregateGroupsRoot(node, { alias, table });
3759
4894
  }
3760
4895
  assertKnownArgs(node, ["where"]);
3761
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4896
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3762
4897
  const whereArg = args.get("where");
3763
4898
  const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
3764
4899
  return {
@@ -3772,10 +4907,10 @@ var OperationLowerer = class {
3772
4907
  /** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
3773
4908
  lowerAggregateGroupsRoot(node, { alias, table }) {
3774
4909
  assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
3775
- if (!node.selectionSet) fail(`'${node.name.value}' needs a selection set`, "invalid-selection");
4910
+ if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
3776
4911
  const args = argsByName(node);
3777
4912
  const groupByArg = args.get("groupBy");
3778
- if (!groupByArg) fail(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
4913
+ if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
3779
4914
  const whereArg = args.get("where");
3780
4915
  const groupBy = this.lowerGroupBy(groupByArg.value, table);
3781
4916
  const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
@@ -3805,55 +4940,55 @@ var OperationLowerer = class {
3805
4940
  */
3806
4941
  lowerGroupBy(node, table) {
3807
4942
  if (node.kind === graphql.Kind.VARIABLE) {
3808
- fail(
4943
+ fail2(
3809
4944
  `groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
3810
4945
  "dynamic-group-by"
3811
4946
  );
3812
4947
  }
3813
4948
  const items = node.kind === graphql.Kind.LIST ? node.values : [node];
3814
- if (items.length === 0) fail(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
4949
+ if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
3815
4950
  const seen = /* @__PURE__ */ new Set();
3816
4951
  return items.map((item) => {
3817
4952
  if (item.kind !== graphql.Kind.OBJECT) {
3818
- fail(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
4953
+ fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
3819
4954
  }
3820
4955
  const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
3821
4956
  for (const key of fields.keys()) {
3822
4957
  if (key !== "column" && key !== "bucket") {
3823
- fail(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
4958
+ fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
3824
4959
  }
3825
4960
  }
3826
4961
  const columnNode = fields.get("column");
3827
- if (!columnNode) fail(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
4962
+ if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
3828
4963
  if (columnNode.kind !== graphql.Kind.ENUM) {
3829
- fail(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
4964
+ fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
3830
4965
  }
3831
4966
  const column = this.column(table, columnNode.value);
3832
4967
  if (!column) {
3833
- fail(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
4968
+ fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3834
4969
  }
3835
4970
  if (column.type === "jsonb") {
3836
- fail(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
4971
+ fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
3837
4972
  }
3838
4973
  const bucketNode = fields.get("bucket");
3839
4974
  let bucket;
3840
4975
  if (bucketNode) {
3841
4976
  if (bucketNode.kind !== graphql.Kind.ENUM) {
3842
- fail(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
4977
+ fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3843
4978
  }
3844
4979
  bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
3845
4980
  if (!bucket) {
3846
- fail(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
4981
+ fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3847
4982
  }
3848
4983
  if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
3849
- fail(
4984
+ fail2(
3850
4985
  `groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
3851
4986
  "invalid-group-by"
3852
4987
  );
3853
4988
  }
3854
4989
  }
3855
4990
  if (seen.has(columnNode.value)) {
3856
- fail(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
4991
+ fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
3857
4992
  }
3858
4993
  seen.add(columnNode.value);
3859
4994
  return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
@@ -3866,10 +5001,10 @@ var OperationLowerer = class {
3866
5001
  if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
3867
5002
  if (node.kind === graphql.Kind.INT) {
3868
5003
  const value = Number(node.value);
3869
- if (value < 1) fail("limit must be a positive integer", "invalid-limit");
5004
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
3870
5005
  return { ...base, value: Math.min(value, MAX_GROUPS) };
3871
5006
  }
3872
- fail("limit must be an integer literal or a variable", "invalid-limit");
5007
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
3873
5008
  }
3874
5009
  /** byPk-style pk arguments: one argument per primary-key column, exactly. */
3875
5010
  lowerPkArgs(node, table) {
@@ -3878,7 +5013,7 @@ var OperationLowerer = class {
3878
5013
  assertKnownArgs(node, meta.primaryKey);
3879
5014
  return meta.primaryKey.map((column) => {
3880
5015
  const arg = args.get(column);
3881
- if (!arg) fail(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
5016
+ if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
3882
5017
  return { column, value: this.lowerValue(arg.value) };
3883
5018
  });
3884
5019
  }
@@ -3889,19 +5024,22 @@ var OperationLowerer = class {
3889
5024
  const name = node.name.value;
3890
5025
  const alias = node.alias?.value ?? name;
3891
5026
  const rootInfo = this.ctx.mutationRoots.get(name);
3892
- if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
5027
+ if (!rootInfo) fail2(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
3893
5028
  if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
3894
5029
  return this.lowerCowRoot(node, rootInfo);
3895
5030
  }
5031
+ if (rootInfo.op === "resolvedWrite") {
5032
+ return this.lowerResolvedRoot(node, rootInfo);
5033
+ }
3896
5034
  const { op, table } = rootInfo;
3897
5035
  const args = argsByName(node);
3898
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
5036
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3899
5037
  const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
3900
5038
  switch (op) {
3901
5039
  case "insert": {
3902
5040
  assertKnownArgs(node, ["objects", "onConflict"]);
3903
5041
  const objects = args.get("objects");
3904
- if (!objects) fail(`'${name}' needs objects:`, "missing-argument");
5042
+ if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
3905
5043
  const response = this.lowerMutationResponse(node.selectionSet, table);
3906
5044
  return {
3907
5045
  ...base,
@@ -3913,7 +5051,7 @@ var OperationLowerer = class {
3913
5051
  case "insertOne": {
3914
5052
  assertKnownArgs(node, ["object", "onConflict"]);
3915
5053
  const object = args.get("object");
3916
- if (!object) fail(`'${name}' needs object:`, "missing-argument");
5054
+ if (!object) fail2(`'${name}' needs object:`, "missing-argument");
3917
5055
  return {
3918
5056
  ...base,
3919
5057
  objects: this.lowerInsertObjects(object.value, table, { list: false }),
@@ -3924,7 +5062,7 @@ var OperationLowerer = class {
3924
5062
  case "update": {
3925
5063
  assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
3926
5064
  const where = args.get("where");
3927
- if (!where) fail(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
5065
+ if (!where) fail2(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
3928
5066
  return {
3929
5067
  ...base,
3930
5068
  ...this.lowerMutationResponse(node.selectionSet, table),
@@ -3935,7 +5073,7 @@ var OperationLowerer = class {
3935
5073
  case "updateByPk": {
3936
5074
  assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
3937
5075
  const pkColumns = args.get("pkColumns");
3938
- if (!pkColumns) fail(`'${name}' needs pkColumns:`, "missing-argument");
5076
+ if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
3939
5077
  return {
3940
5078
  ...base,
3941
5079
  pk: this.lowerPkColumnsArg(pkColumns.value, table),
@@ -3946,7 +5084,7 @@ var OperationLowerer = class {
3946
5084
  case "delete": {
3947
5085
  assertKnownArgs(node, ["where"]);
3948
5086
  const where = args.get("where");
3949
- if (!where) fail(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
5087
+ if (!where) fail2(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
3950
5088
  return {
3951
5089
  ...base,
3952
5090
  ...this.lowerMutationResponse(node.selectionSet, table),
@@ -3973,11 +5111,11 @@ var OperationLowerer = class {
3973
5111
  const name = node.name.value;
3974
5112
  const alias = node.alias?.value ?? name;
3975
5113
  const { table } = rootInfo;
3976
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
5114
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3977
5115
  const meta = this.meta(table);
3978
5116
  const args = argsByName(node);
3979
5117
  const pkArg = args.get(rootInfo.pk);
3980
- if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
5118
+ if (!pkArg) fail2(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
3981
5119
  const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
3982
5120
  const base = {
3983
5121
  kind: "mutation",
@@ -3998,24 +5136,78 @@ var OperationLowerer = class {
3998
5136
  const entries = rootInfo.set.map((column) => {
3999
5137
  const arg = args.get(column);
4000
5138
  if (!arg) {
4001
- fail(
5139
+ fail2(
4002
5140
  `'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
4003
5141
  "missing-argument",
4004
5142
  [...rootInfo.set]
4005
5143
  );
4006
5144
  }
4007
5145
  if (!meta.settable.includes(column)) {
4008
- fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
5146
+ fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
4009
5147
  }
4010
5148
  return { column, value: this.lowerValue(arg.value) };
4011
5149
  });
4012
5150
  return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
4013
5151
  }
5152
+ /**
5153
+ * Resolved-table roots: insertOne(object), updateByPk(pkColumns, _set),
5154
+ * deleteByPk(pk…), revertByPk(pkColumns, columns). The executor writes only
5155
+ * the touched cells into the manual source; `returning` reads the view.
5156
+ */
5157
+ lowerResolvedRoot(node, rootInfo) {
5158
+ const name = node.name.value;
5159
+ const alias = node.alias?.value ?? name;
5160
+ const { table, action } = rootInfo;
5161
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
5162
+ const args = argsByName(node);
5163
+ const base = {
5164
+ kind: "mutation",
5165
+ op: "resolvedWrite",
5166
+ resolvedAction: action,
5167
+ alias,
5168
+ table,
5169
+ pinnedColumns: [],
5170
+ affectedRows: false,
5171
+ returning: this.lowerSelection(node.selectionSet, table)
5172
+ };
5173
+ switch (action) {
5174
+ case "insert": {
5175
+ assertKnownArgs(node, ["object"]);
5176
+ const object = args.get("object");
5177
+ if (!object) fail2(`'${name}' needs object:`, "missing-argument");
5178
+ return { ...base, objects: this.lowerInsertObjects(object.value, table, { list: false }) };
5179
+ }
5180
+ case "update": {
5181
+ assertKnownArgs(node, ["pkColumns", "_set"]);
5182
+ const pkColumns = args.get("pkColumns");
5183
+ if (!pkColumns || !args.get("_set")) fail2(`'${name}' needs pkColumns: and _set:`, "missing-argument");
5184
+ return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), ...this.lowerWriteValues(args, table) };
5185
+ }
5186
+ case "delete":
5187
+ return { ...base, pk: this.lowerPkArgs(node, table) };
5188
+ case "revert": {
5189
+ assertKnownArgs(node, ["pkColumns", "columns"]);
5190
+ const pkColumns = args.get("pkColumns");
5191
+ const columns = args.get("columns");
5192
+ if (!pkColumns || !columns) fail2(`'${name}' needs pkColumns: and columns:`, "missing-argument");
5193
+ const items = columns.value.kind === graphql.Kind.LIST ? columns.value.values : [columns.value];
5194
+ const meta = this.meta(table);
5195
+ const revertColumns = items.map((item) => {
5196
+ if (item.kind !== graphql.Kind.ENUM || !meta.settable.includes(item.value)) {
5197
+ fail2(`'${name}' columns: must list update-column enum values written in the document`, "invalid-revert-columns", meta.settable);
5198
+ }
5199
+ return item.value;
5200
+ });
5201
+ if (revertColumns.length === 0) fail2(`'${name}' columns: must name at least one column`, "invalid-revert-columns");
5202
+ return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), revertColumns: [...new Set(revertColumns)] };
5203
+ }
5204
+ }
5205
+ }
4014
5206
  lowerMutationResponse(selectionSet, table) {
4015
5207
  let affectedRows = false;
4016
5208
  let returning = null;
4017
5209
  for (const node of selectionSet.selections) {
4018
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
5210
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
4019
5211
  const name = node.name.value;
4020
5212
  if (name === "__typename") continue;
4021
5213
  if (name === "affectedRows") {
@@ -4023,34 +5215,34 @@ var OperationLowerer = class {
4023
5215
  continue;
4024
5216
  }
4025
5217
  if (name === "returning") {
4026
- if (!node.selectionSet) fail("'returning' needs a selection set", "invalid-selection");
5218
+ if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
4027
5219
  returning = this.lowerSelection(node.selectionSet, table);
4028
5220
  continue;
4029
5221
  }
4030
- fail(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
5222
+ fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
4031
5223
  }
4032
5224
  return { affectedRows, returning };
4033
5225
  }
4034
5226
  lowerInsertObjects(node, table, { list }) {
4035
5227
  if (node.kind === graphql.Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
4036
5228
  if (list) {
4037
- if (node.kind !== graphql.Kind.LIST) fail(`objects: must be a list literal or a variable`, "invalid-insert");
5229
+ if (node.kind !== graphql.Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
4038
5230
  return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
4039
5231
  }
4040
5232
  return { rows: [this.lowerInsertRow(node, table)] };
4041
5233
  }
4042
5234
  lowerInsertRow(node, table) {
4043
- if (node.kind !== graphql.Kind.OBJECT) fail(`insert rows on '${table}' must be objects`, "invalid-insert");
5235
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
4044
5236
  const meta = this.meta(table);
4045
5237
  return node.fields.map((field) => {
4046
5238
  const key = field.name.value;
4047
5239
  if (this.link(table, key)) {
4048
- fail(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
5240
+ fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
4049
5241
  }
4050
5242
  if (!meta.insertable.includes(key)) {
4051
5243
  const column = this.column(table, key);
4052
- if (column) fail(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
4053
- fail(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
5244
+ if (column) fail2(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
5245
+ fail2(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
4054
5246
  }
4055
5247
  return { column: key, value: this.lowerValue(field.value) };
4056
5248
  });
@@ -4059,32 +5251,32 @@ var OperationLowerer = class {
4059
5251
  if (!arg) return {};
4060
5252
  const node = arg.value;
4061
5253
  if (node.kind === graphql.Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
4062
- if (node.kind !== graphql.Kind.OBJECT) fail("onConflict: must be an object literal or a variable", "invalid-on-conflict");
5254
+ if (node.kind !== graphql.Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
4063
5255
  const meta = this.meta(table);
4064
5256
  let constraint;
4065
5257
  const updateColumns = [];
4066
5258
  for (const field of node.fields) {
4067
5259
  if (field.name.value === "constraint") {
4068
- if (field.value.kind !== graphql.Kind.ENUM) fail("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
5260
+ if (field.value.kind !== graphql.Kind.ENUM) fail2("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
4069
5261
  constraint = field.value.value;
4070
5262
  continue;
4071
5263
  }
4072
5264
  if (field.name.value === "updateColumns") {
4073
5265
  const items = field.value.kind === graphql.Kind.LIST ? field.value.values : [field.value];
4074
5266
  for (const item of items) {
4075
- if (item.kind !== graphql.Kind.ENUM) fail("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
5267
+ if (item.kind !== graphql.Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
4076
5268
  if (!meta.settable.includes(item.value)) {
4077
- fail(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
5269
+ fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
4078
5270
  }
4079
5271
  updateColumns.push(item.value);
4080
5272
  }
4081
5273
  continue;
4082
5274
  }
4083
- fail(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
5275
+ fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
4084
5276
  }
4085
- if (!constraint) fail("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
5277
+ if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
4086
5278
  if (!meta.constraints.some((c) => c.name === constraint)) {
4087
- fail(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
5279
+ fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
4088
5280
  }
4089
5281
  return { onConflict: { constraint, updateColumns } };
4090
5282
  }
@@ -4102,36 +5294,36 @@ var OperationLowerer = class {
4102
5294
  const inc = args.get("_inc");
4103
5295
  if (inc) {
4104
5296
  if (inc.value.kind === graphql.Kind.VARIABLE) {
4105
- fail("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
5297
+ fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
4106
5298
  }
4107
5299
  const numeric4 = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
4108
5300
  result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric4, "_inc") };
4109
5301
  }
4110
5302
  if (!result.set && !result.append && !result.inc) {
4111
- fail(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
5303
+ fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
4112
5304
  }
4113
5305
  return result;
4114
5306
  }
4115
5307
  lowerColumnEntries(node, table, whitelist, argName) {
4116
- if (node.kind !== graphql.Kind.OBJECT) fail(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
5308
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
4117
5309
  return node.fields.map((field) => {
4118
5310
  const key = field.name.value;
4119
5311
  if (!whitelist.includes(key)) {
4120
- fail(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
5312
+ fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
4121
5313
  }
4122
5314
  return { column: key, value: this.lowerValue(field.value) };
4123
5315
  });
4124
5316
  }
4125
5317
  lowerPkColumnsArg(node, table) {
4126
- if (node.kind !== graphql.Kind.OBJECT) fail(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
5318
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
4127
5319
  const meta = this.meta(table);
4128
5320
  const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
4129
5321
  for (const key of provided.keys()) {
4130
- if (!meta.primaryKey.includes(key)) fail(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
5322
+ if (!meta.primaryKey.includes(key)) fail2(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
4131
5323
  }
4132
5324
  return meta.primaryKey.map((column) => {
4133
5325
  const value = provided.get(column);
4134
- if (!value) fail(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
5326
+ if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
4135
5327
  return { column, value: this.lowerValue(value) };
4136
5328
  });
4137
5329
  }
@@ -4140,6 +5332,19 @@ function lowerOperation({ restricted, ctx }) {
4140
5332
  const lowerer = new OperationLowerer(ctx, restricted);
4141
5333
  return lowerer.lowerRoots(restricted.definition, restricted.kind);
4142
5334
  }
5335
+ function isCellColumn(meta, name) {
5336
+ if (!meta.cells) return false;
5337
+ return Object.values(meta.cells).some((parts) => Object.values(parts).includes(name));
5338
+ }
5339
+ function fieldNodes(selectionSet) {
5340
+ const out = [];
5341
+ for (const node of selectionSet.selections) {
5342
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
5343
+ if (node.directives && node.directives.length > 0) fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
5344
+ if (node.name.value !== "__typename") out.push(node);
5345
+ }
5346
+ return out;
5347
+ }
4143
5348
  function parseSdl(sdl) {
4144
5349
  return graphql.buildSchema(sdl);
4145
5350
  }
@@ -4516,15 +5721,6 @@ function columnWireType(column) {
4516
5721
  const notNull = column.primary_key === true || column.nullable === false;
4517
5722
  return notNull || base === "unknown" ? base : `${base} | null`;
4518
5723
  }
4519
-
4520
- // src/compiler/sql.ts
4521
- var CLAIMS_SETTING = "request.jwt.claims";
4522
- var CAPABILITIES_CLAIM_KEY = "capabilities";
4523
- var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
4524
- var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
4525
- var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
4526
- var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
4527
- var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
4528
5724
  var identifierSchema = zod.z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
4529
5725
  var namespaceSchema = zod.z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
4530
5726
  var tableReferenceSchema = zod.z.string().refine(
@@ -4641,7 +5837,7 @@ var OPERATOR_PATTERN2 = /^(!=|<=|>=|<>|=|<|>)/;
4641
5837
  var IDENTIFIER_START2 = /[a-z_]/;
4642
5838
  var IDENTIFIER_CHAR2 = /[a-z0-9_.-]/;
4643
5839
  var NUMBER_PATTERN2 = /^-?\d+(\.\d+)?/;
4644
- function tokenize2(source) {
5840
+ function tokenize3(source) {
4645
5841
  const tokens = [];
4646
5842
  let position = 0;
4647
5843
  while (position < source.length) {
@@ -4715,7 +5911,7 @@ function readString2(source, start) {
4715
5911
  }
4716
5912
  throw new PredicateParseError("unterminated string literal", start);
4717
5913
  }
4718
- var Parser2 = class {
5914
+ var Parser3 = class {
4719
5915
  constructor(tokens) {
4720
5916
  this.tokens = tokens;
4721
5917
  }
@@ -4847,7 +6043,7 @@ var Parser2 = class {
4847
6043
  function parsePredicate(source) {
4848
6044
  const trimmed = source.trim();
4849
6045
  if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
4850
- return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
6046
+ return { source: trimmed, ast: new Parser3(tokenize3(trimmed)).parse() };
4851
6047
  }
4852
6048
 
4853
6049
  // src/schema/predicates.ts
@@ -5569,6 +6765,88 @@ function snapshotTableNames(snapshot, schema) {
5569
6765
  return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
5570
6766
  }
5571
6767
 
6768
+ // src/engine/materialized-refresh.ts
6769
+ function materializedRefreshFunctions(canonical) {
6770
+ const schema = ormSchemaName(canonical.namespace);
6771
+ const functions = /* @__PURE__ */ new Map();
6772
+ for (const view of canonical.views ?? []) {
6773
+ const materialized = view.resolved?.materialized;
6774
+ if (!materialized) continue;
6775
+ const table = canonical.tables.find((candidate) => candidate.table === materialized.table);
6776
+ const pk = view.primaryKey ?? [];
6777
+ if (!table || pk.length === 0) {
6778
+ throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
6779
+ }
6780
+ functions.set(view.view, refreshFunctionSql({
6781
+ schema,
6782
+ name: view.view,
6783
+ table: materialized.table,
6784
+ compute: materialized.compute,
6785
+ columns: table.columns.map((column) => column.columnName),
6786
+ pk
6787
+ }));
6788
+ }
6789
+ return functions;
6790
+ }
6791
+ function refreshFunctionSql({ schema, name, table, compute, columns, pk }) {
6792
+ const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name))}`;
6793
+ const target = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
6794
+ const source = `${quoteIdentifier(schema)}.${quoteIdentifier(compute)}`;
6795
+ const list = columns.map(quoteIdentifier).join(", ");
6796
+ const others = columns.filter((column) => !pk.includes(column));
6797
+ const keys = pk.map(quoteIdentifier).join(", ");
6798
+ const matches = pk.map((column) => `c.${quoteIdentifier(column)} = m.${quoteIdentifier(column)}`).join(" AND ");
6799
+ const onConflict = others.length === 0 ? "DO NOTHING" : `DO UPDATE SET ${others.map((column) => `${quoteIdentifier(column)} = EXCLUDED.${quoteIdentifier(column)}`).join(", ")}
6800
+ WHERE (${others.map((column) => `m.${quoteIdentifier(column)}`).join(", ")}) IS DISTINCT FROM (${others.map((column) => `EXCLUDED.${quoteIdentifier(column)}`).join(", ")})`;
6801
+ const lockKey = quoteLiteral(materializeLockKey(schema, name));
6802
+ return `CREATE OR REPLACE FUNCTION ${fn}(wait boolean DEFAULT false)
6803
+ RETURNS TABLE(upserted bigint, deleted bigint, locked boolean)
6804
+ LANGUAGE plpgsql
6805
+ SECURITY INVOKER
6806
+ SET search_path = pg_catalog, pg_temp
6807
+ AS $orm_refresh$
6808
+ #variable_conflict use_column
6809
+ DECLARE
6810
+ caller_claims text := current_setting('request.jwt.claims', true);
6811
+ BEGIN
6812
+ IF wait THEN
6813
+ PERFORM pg_advisory_xact_lock(hashtext(${lockKey}));
6814
+ ELSIF NOT pg_try_advisory_xact_lock(hashtext(${lockKey})) THEN
6815
+ RETURN QUERY SELECT 0::bigint, 0::bigint, false;
6816
+ RETURN;
6817
+ END IF;
6818
+ PERFORM set_config('request.jwt.claims', ${quoteLiteral(MATERIALIZE_CLAIMS_JSON)}, true);
6819
+ RETURN QUERY
6820
+ WITH up AS (
6821
+ INSERT INTO ${target} AS m (${list})
6822
+ SELECT ${list} FROM ${source}
6823
+ ON CONFLICT (${keys}) ${onConflict}
6824
+ RETURNING 1
6825
+ ), gone AS (
6826
+ DELETE FROM ${target} AS m
6827
+ WHERE NOT EXISTS (SELECT 1 FROM ${source} AS c WHERE ${matches})
6828
+ RETURNING 1
6829
+ )
6830
+ SELECT (SELECT count(*) FROM up), (SELECT count(*) FROM gone), true;
6831
+ -- NULL resets to the session value when the caller had none.
6832
+ PERFORM set_config('request.jwt.claims', caller_claims, true);
6833
+ END
6834
+ $orm_refresh$`;
6835
+ }
6836
+ function refreshFunctionStatements({ namespace, previous, current }) {
6837
+ const schema = quoteIdentifier(ormSchemaName(namespace));
6838
+ const statements = [];
6839
+ for (const name of Object.keys(previous).sort()) {
6840
+ if (!current.has(name)) statements.push(`DROP FUNCTION IF EXISTS ${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`);
6841
+ }
6842
+ for (const [name, create] of [...current].sort(([a], [b]) => a.localeCompare(b))) {
6843
+ if (previous[name] === create) continue;
6844
+ const fn = `${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`;
6845
+ statements.push(create, `REVOKE ALL ON FUNCTION ${fn} FROM PUBLIC`);
6846
+ }
6847
+ return statements;
6848
+ }
6849
+
5572
6850
  // src/engine/canonical2.ts
5573
6851
  var FACTORIES2 = {
5574
6852
  uuid: (name) => pgCore.uuid(name),
@@ -5860,8 +7138,10 @@ function generateSnapshotV2(definition, previousId3) {
5860
7138
  const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
5861
7139
  if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
5862
7140
  const extended = snapshot;
7141
+ const refreshFunctions = materializedRefreshFunctions(definition);
5863
7142
  extended._sequenceOrm = {
5864
7143
  ...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
7144
+ ...refreshFunctions.size > 0 ? { refreshFunctions: Object.fromEntries(refreshFunctions) } : {},
5865
7145
  viewDependencies: Object.fromEntries(
5866
7146
  (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
5867
7147
  )
@@ -5901,15 +7181,19 @@ function buildTable2(schema, table, built, enums, byName, introspection) {
5901
7181
  const drizzleTable = schema.table(table.table, columns, (self) => [
5902
7182
  ...buildCompositePrimaryKey(table, primaryKeyColumns2, self),
5903
7183
  ...buildIndexes2(table, self),
7184
+ ...(table.checks ?? []).map((check) => pgCore.check(check.name, drizzleOrm.sql.raw(compileCheckExpression(check.expression, table.columns)))),
5904
7185
  ...buildForeignKeys2(table, self, built, byName),
5905
7186
  ...(table.foreignKeys ?? []).map((fk) => {
5906
7187
  const target = fk.target === table.table ? self : built.get(fk.target);
5907
7188
  if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
5908
- return pgCore.foreignKey({
7189
+ let constraint = pgCore.foreignKey({
5909
7190
  name: fk.name,
5910
7191
  columns: fk.columns.map((column) => self[column]),
5911
7192
  foreignColumns: fk.references.map((column) => target[column])
5912
7193
  });
7194
+ if (fk.onDelete !== void 0) constraint = constraint.onDelete(fk.onDelete);
7195
+ if (fk.onUpdate !== void 0) constraint = constraint.onUpdate(fk.onUpdate);
7196
+ return constraint;
5913
7197
  })
5914
7198
  ]);
5915
7199
  return drizzleTable.enableRLS();
@@ -6000,11 +7284,14 @@ function buildForeignKeys2(table, self, built, byName) {
6000
7284
  `enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
6001
7285
  );
6002
7286
  }
6003
- return pgCore.foreignKey({
7287
+ let constraint = pgCore.foreignKey({
6004
7288
  name: foreignKeyName(table.table, link.name),
6005
7289
  columns: [sourceColumn],
6006
7290
  foreignColumns: [targetColumn]
6007
7291
  });
7292
+ if (link.onDelete !== void 0) constraint = constraint.onDelete(link.onDelete);
7293
+ if (link.onUpdate !== void 0) constraint = constraint.onUpdate(link.onUpdate);
7294
+ return constraint;
6008
7295
  });
6009
7296
  }
6010
7297
  var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
@@ -6125,9 +7412,23 @@ function findTable2(snapshot, schema, table) {
6125
7412
  const tables = snapshot.tables ?? {};
6126
7413
  return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
6127
7414
  }
6128
- async function planMigrationV2({
7415
+ function planMigrationV2(input) {
7416
+ return planWithViewRecreation({ ...input, viewRecreation: "affected" });
7417
+ }
7418
+ async function planCommittedMigrationV2({
6129
7419
  canonical,
6130
- previousSnapshot
7420
+ previousSnapshot,
7421
+ reproduces
7422
+ }) {
7423
+ const current = await planMigrationV2({ canonical, previousSnapshot });
7424
+ if (reproduces(current)) return { plan: current, reproduced: true };
7425
+ const legacy = await planWithViewRecreation({ canonical, previousSnapshot, viewRecreation: "all-on-base-change" });
7426
+ return reproduces(legacy) ? { plan: legacy, reproduced: true } : { plan: current, reproduced: false };
7427
+ }
7428
+ async function planWithViewRecreation({
7429
+ canonical,
7430
+ previousSnapshot,
7431
+ viewRecreation
6131
7432
  }) {
6132
7433
  const schema = ormSchemaName(canonical.namespace);
6133
7434
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
@@ -6145,7 +7446,14 @@ async function planMigrationV2({
6145
7446
  currentViews,
6146
7447
  previousDependencies,
6147
7448
  currentDependencies,
6148
- baseChanged: hints.statements.length > 0 || baseDdl.length > 0
7449
+ reach: viewReach({
7450
+ viewRecreation,
7451
+ hintStatements: hints.statements,
7452
+ baseDdl,
7453
+ previous,
7454
+ adjusted: hints.adjusted,
7455
+ current: snapshot
7456
+ })
6149
7457
  });
6150
7458
  const dropOrder = topologicalViewNames(
6151
7459
  new Set([...affected].filter((name) => previousViews.has(name))),
@@ -6178,7 +7486,12 @@ async function planMigrationV2({
6178
7486
  const view = currentViewDefinitions.get(name);
6179
7487
  if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
6180
7488
  return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
6181
- })
7489
+ }),
7490
+ ...refreshFunctionStatements({
7491
+ namespace: canonical.namespace,
7492
+ previous: previous._sequenceOrm?.refreshFunctions ?? {},
7493
+ current: materializedRefreshFunctions(canonical)
7494
+ }).map((sql4) => ({ kind: "ddl", sql: sql4 }))
6182
7495
  ];
6183
7496
  return { namespace: canonical.namespace, schema, statements, snapshot };
6184
7497
  }
@@ -6332,20 +7645,102 @@ function withoutViews(snapshot) {
6332
7645
  delete clone._sequenceOrm;
6333
7646
  return clone;
6334
7647
  }
7648
+ var DROPS_TYPE = /^\s*DROP\s+TYPE\b/i;
7649
+ function viewReach({
7650
+ viewRecreation,
7651
+ hintStatements,
7652
+ baseDdl,
7653
+ previous,
7654
+ adjusted,
7655
+ current
7656
+ }) {
7657
+ if (hintStatements.length === 0 && baseDdl.length === 0) return /* @__PURE__ */ new Set();
7658
+ if (viewRecreation === "all-on-base-change") return "all";
7659
+ if (baseDdl.some((statement) => DROPS_TYPE.test(statement))) return "all";
7660
+ const dependencies = snapshotViewDependencies(adjusted);
7661
+ if ([...snapshotViews(adjusted).keys()].some((name) => dependencies[name] === void 0)) return "all";
7662
+ return baseChangesReachingViews({ previous, adjusted, current });
7663
+ }
7664
+ var SNAPSHOT_BOOKKEEPING = /* @__PURE__ */ new Set(["id", "prevId", "version", "dialect", "_meta", "views", "_sequenceOrm", "enums"]);
7665
+ var VIEW_SAFE_TABLE_FIELDS = /* @__PURE__ */ new Set([
7666
+ "indexes",
7667
+ "foreignKeys",
7668
+ "uniqueConstraints",
7669
+ "checkConstraints",
7670
+ "policies",
7671
+ "isRLSEnabled"
7672
+ ]);
7673
+ var VIEW_SAFE_COLUMN_FIELDS = /* @__PURE__ */ new Set(["notNull", "default", "isUnique", "uniqueName", "nullsNotDistinct"]);
7674
+ function baseChangesReachingViews({
7675
+ previous,
7676
+ adjusted,
7677
+ current
7678
+ }) {
7679
+ const before = adjusted;
7680
+ const after = current;
7681
+ for (const section of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
7682
+ if (SNAPSHOT_BOOKKEEPING.has(section) || section === "tables") continue;
7683
+ if (stableJson(before[section]) !== stableJson(after[section])) return "all";
7684
+ }
7685
+ const relations = /* @__PURE__ */ new Set();
7686
+ const previousTables = snapshotTables(previous);
7687
+ const adjustedTables = snapshotTables(adjusted);
7688
+ for (const key of changedEntryKeys(previousTables, adjustedTables)) {
7689
+ const table = previousTables[key] ?? adjustedTables[key];
7690
+ if (table) relations.add(table.name);
7691
+ }
7692
+ const currentTables = snapshotTables(current);
7693
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(adjustedTables), ...Object.keys(currentTables)])) {
7694
+ const was = adjustedTables[key];
7695
+ const is = currentTables[key];
7696
+ if (!was || !is) {
7697
+ const table = was ?? is;
7698
+ if (table) relations.add(table.name);
7699
+ } else if (tableChangeReachesViews({ was, is })) {
7700
+ relations.add(is.name);
7701
+ }
7702
+ }
7703
+ return relations;
7704
+ }
7705
+ function tableChangeReachesViews({ was, is }) {
7706
+ for (const field of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(is)])) {
7707
+ if (field === "columns" || VIEW_SAFE_TABLE_FIELDS.has(field)) continue;
7708
+ if (stableJson(was[field]) !== stableJson(is[field])) return true;
7709
+ }
7710
+ return Object.entries(was.columns).some(([name, column]) => {
7711
+ const next = is.columns[name];
7712
+ if (!next) return true;
7713
+ return [.../* @__PURE__ */ new Set([...Object.keys(column), ...Object.keys(next)])].some(
7714
+ (field) => !VIEW_SAFE_COLUMN_FIELDS.has(field) && stableJson(column[field]) !== stableJson(next[field])
7715
+ );
7716
+ });
7717
+ }
7718
+ function changedEntryKeys(before, after) {
7719
+ return new Set(
7720
+ [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter(
7721
+ (key) => stableJson(before[key]) !== stableJson(after[key])
7722
+ )
7723
+ );
7724
+ }
7725
+ function snapshotTables(snapshot) {
7726
+ return snapshot.tables ?? {};
7727
+ }
6335
7728
  function affectedViewNames({
6336
7729
  previousViews,
6337
7730
  currentViews,
6338
7731
  previousDependencies,
6339
7732
  currentDependencies,
6340
- baseChanged
7733
+ reach
6341
7734
  }) {
6342
7735
  const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
6343
- if (baseChanged) return allNames;
7736
+ if (reach === "all") return allNames;
7737
+ const dependenciesOf = (name) => /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
6344
7738
  const affected = new Set(
6345
7739
  [...allNames].filter((name) => {
6346
7740
  const before = previousViews.get(name);
6347
7741
  const after = currentViews.get(name);
6348
- return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
7742
+ if (before === void 0 || after === void 0 || stableJson(before) !== stableJson(after)) return true;
7743
+ return [...dependenciesOf(name)].some((dependency) => reach.has(dependency));
6349
7744
  })
6350
7745
  );
6351
7746
  let grew = true;
@@ -6353,8 +7748,7 @@ function affectedViewNames({
6353
7748
  grew = false;
6354
7749
  for (const name of allNames) {
6355
7750
  if (affected.has(name)) continue;
6356
- const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
6357
- if ([...dependencies].some((dependency) => affected.has(dependency))) {
7751
+ if ([...dependenciesOf(name)].some((dependency) => affected.has(dependency))) {
6358
7752
  affected.add(name);
6359
7753
  grew = true;
6360
7754
  }
@@ -7269,7 +8663,9 @@ var PLATFORM_USER_FIELDS = [
7269
8663
  ];
7270
8664
  function generateSdl(canonical, options = {}) {
7271
8665
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
7272
- const tables = canonicalRelations(def).filter((table) => table.source === void 0);
8666
+ const exposed = canonicalRelations(def).filter((table) => table.source === void 0 && table.internal !== true);
8667
+ const cellsByTable = new Map(exposed.map((table) => [table.table, trackedCells(table)]));
8668
+ const tables = exposed.map((table) => ({ ...table, columns: table.columns.filter((column) => column.cell === void 0) }));
7273
8669
  if (tables.length === 0) {
7274
8670
  throw new SdlGenerationError(
7275
8671
  `Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
@@ -7328,7 +8724,9 @@ function generateSdl(canonical, options = {}) {
7328
8724
  blocks.push(
7329
8725
  '"""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'
7330
8726
  );
8727
+ if (tables.some((table) => table.resolved !== void 0)) blocks.push(...RESOLVED_DIRECTIVE_BLOCKS);
7331
8728
  for (const scalar2 of CUSTOM_SCALARS) blocks.push(`scalar ${scalar2}`);
8729
+ blocks.push(...renderSharedCellTypes([...cellsByTable.values()]));
7332
8730
  blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
7333
8731
  blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
7334
8732
  for (const enumName of [...usedEnums].sort()) {
@@ -7353,7 +8751,8 @@ function generateSdl(canonical, options = {}) {
7353
8751
  names,
7354
8752
  namesByTable,
7355
8753
  emitAggregateBoolExp: aggregateBoolExpTargets.has(table.table),
7356
- pinned: options.pinnedColumns?.[table.table] ?? []
8754
+ pinned: options.pinnedColumns?.[table.table] ?? [],
8755
+ cells: cellsByTable.get(table.table) ?? null
7357
8756
  })
7358
8757
  );
7359
8758
  }
@@ -7443,6 +8842,52 @@ ${orderFields}
7443
8842
  }`
7444
8843
  ];
7445
8844
  }
8845
+ var RESOLVED_DIRECTIVE_BLOCKS = [
8846
+ '"""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',
8847
+ renderEnum("WritebackMode", ["SYNC", "ASYNC"]),
8848
+ '"""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',
8849
+ '"""Declare that this mutation deliberately keeps writeback columns local to the named source."""\ndirective @localOnly(source: String!) repeatable on FIELD'
8850
+ ];
8851
+ function trackedCells(table) {
8852
+ const columns = [...new Set(table.columns.flatMap((column) => column.cell ? [column.cell.of] : []))];
8853
+ if (columns.length === 0) return null;
8854
+ return { resolved: table.columns.some((column) => column.cell?.part === "source"), columns };
8855
+ }
8856
+ function renderSharedCellTypes(all) {
8857
+ const present = all.filter((cells) => cells !== null);
8858
+ const blocks = [];
8859
+ if (present.some((cells) => !cells.resolved)) {
8860
+ blocks.push(`"""When, and by whom, a cell was last written."""
8861
+ type ${CELL_TYPENAMES.stamp} {
8862
+ at: timestamptz!
8863
+ by: String
8864
+ }`);
8865
+ }
8866
+ if (present.some((cells) => cells.resolved)) {
8867
+ blocks.push(`"""A newer candidate value that lost to the column's resolve rule."""
8868
+ type ${CELL_TYPENAMES.conflict} {
8869
+ source: String!
8870
+ at: timestamptz
8871
+ value: jsonb
8872
+ }`);
8873
+ blocks.push(`"""How a resolved cell was decided: the winning source, its time and actor, and newer losers."""
8874
+ type ${CELL_TYPENAMES.resolved} {
8875
+ source: String!
8876
+ at: timestamptz
8877
+ by: String
8878
+ conflicts: [${CELL_TYPENAMES.conflict}!]!
8879
+ }`);
8880
+ }
8881
+ return blocks;
8882
+ }
8883
+ function renderCellsType(ctx) {
8884
+ if (!ctx.cells) return [];
8885
+ const cellType = ctx.cells.resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp;
8886
+ const lines = ctx.cells.columns.map((column) => ` ${fieldName(column)}: ${cellType}`);
8887
+ return [`type ${cellsTypename(ctx.names.typename)} {
8888
+ ${lines.join("\n")}
8889
+ }`];
8890
+ }
7446
8891
  function relationArgs(target) {
7447
8892
  return `where: ${target.boolExp}, orderBy: [${target.orderBy}!], limit: Int, offset: Int`;
7448
8893
  }
@@ -7451,10 +8896,10 @@ function rootArgs(names) {
7451
8896
  }
7452
8897
  function primaryKeyColumns(table) {
7453
8898
  if (!table.primaryKey || table.primaryKey.length === 0) return [];
7454
- return table.primaryKey.map((identifier2) => {
7455
- const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
8899
+ return table.primaryKey.map((identifier3) => {
8900
+ const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
7456
8901
  if (!column) {
7457
- throw new SdlGenerationError(`Primary key column "${identifier2}" not found on table "${table.table}".`);
8902
+ throw new SdlGenerationError(`Primary key column "${identifier3}" not found on table "${table.table}".`);
7458
8903
  }
7459
8904
  return column;
7460
8905
  });
@@ -7463,6 +8908,7 @@ function renderTableBlocks(ctx) {
7463
8908
  const { table, names } = ctx;
7464
8909
  const blocks = [
7465
8910
  renderObjectType(ctx),
8911
+ ...renderCellsType(ctx),
7466
8912
  renderSelectColumnEnum(ctx),
7467
8913
  renderOrderByInput(ctx),
7468
8914
  renderBoolExp(ctx),
@@ -7477,11 +8923,49 @@ function renderTableBlocks(ctx) {
7477
8923
  ${fields}
7478
8924
  }`);
7479
8925
  }
7480
- if (allowsInsert(table) || allowsUpdate(table)) {
8926
+ if (ctx.table.resolved !== void 0) {
8927
+ blocks.push(...renderResolvedWriteBlocks(ctx));
8928
+ } else if (allowsInsert(table) || allowsUpdate(table)) {
7481
8929
  blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
7482
8930
  }
7483
8931
  return blocks;
7484
8932
  }
8933
+ function resolvedWritableColumns(table) {
8934
+ const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
8935
+ return table.columns.filter((column) => !system.has(column.name));
8936
+ }
8937
+ function renderResolvedWriteBlocks(ctx) {
8938
+ const { table, names } = ctx;
8939
+ const writable = resolvedWritableColumns(table);
8940
+ const pk = new Set(table.primaryKey ?? []);
8941
+ const settable = writable.filter((column) => !pk.has(column.columnName));
8942
+ const line = (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`;
8943
+ const blocks = [];
8944
+ if (allowsInsert(table)) blocks.push(`input ${names.insertInput} {
8945
+ ${writable.map(line).join("\n")}
8946
+ }`);
8947
+ if (allowsUpdate(table)) {
8948
+ blocks.push(`input ${names.setInput} {
8949
+ ${settable.map(line).join("\n")}
8950
+ }`);
8951
+ blocks.push(renderEnum(names.updateColumnEnum, settable.map((column) => fieldName(column.name))));
8952
+ }
8953
+ return blocks;
8954
+ }
8955
+ function renderResolvedMutationFields(table, names) {
8956
+ const lines = [];
8957
+ const pk = primaryKeyColumns(table);
8958
+ if (allowsInsert(table)) lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!): ${names.typename}`);
8959
+ if (allowsUpdate(table) && pk.length > 0) {
8960
+ lines.push(` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, _set: ${names.setInput}!): ${names.typename}`);
8961
+ lines.push(` ${names.revertByPkRoot}(pkColumns: ${names.pkColumnsInput}!, columns: [${names.updateColumnEnum}!]!): ${names.typename}`);
8962
+ if ((table.resolved?.deletedBy.length ?? 0) > 0) {
8963
+ const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
8964
+ lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
8965
+ }
8966
+ }
8967
+ return lines;
8968
+ }
7485
8969
  function allowsInsert(table) {
7486
8970
  return table.policies.write !== void 0 || table.policies.insert !== void 0;
7487
8971
  }
@@ -7504,6 +8988,10 @@ function renderObjectType(ctx) {
7504
8988
  const deprecated = column.deprecated ? " @deprecated" : "";
7505
8989
  lines.push(` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}${bang}${deprecated}`);
7506
8990
  }
8991
+ if (ctx.cells) {
8992
+ lines.push(' """Per-cell metadata: when each field was last written, and by whom."""');
8993
+ lines.push(` ${CELLS_FIELD}: ${cellsTypename(names.typename)}!`);
8994
+ }
7507
8995
  for (const link of table.links) {
7508
8996
  if (link.description !== void 0) lines.push(...descriptionLines(link.description, " "));
7509
8997
  if (link.kind === "toPlatformUser") {
@@ -7572,7 +9060,10 @@ function numericishColumns(table) {
7572
9060
  return table.columns.filter((column) => typeof column.type !== "object" && NUMERICISH.has(column.type));
7573
9061
  }
7574
9062
  function jsonbColumns(table) {
7575
- return table.columns.filter((column) => column.type === "jsonb");
9063
+ return clientWritableColumns(table).filter((column) => column.type === "jsonb");
9064
+ }
9065
+ function clientWritableColumns(table) {
9066
+ return table.columns;
7576
9067
  }
7577
9068
  function renderAggregateTypes(ctx) {
7578
9069
  const { table, names } = ctx;
@@ -7656,14 +9147,14 @@ function constraintValues(table) {
7656
9147
  return [...values].sort();
7657
9148
  }
7658
9149
  function clientSettableColumns(table) {
7659
- return table.columns.filter((column) => column.onUpdateFn === void 0);
9150
+ return clientWritableColumns(table).filter((column) => column.onUpdateFn === void 0);
7660
9151
  }
7661
9152
  function renderWriteBlocks(ctx, access) {
7662
9153
  const { table, names, pinned } = ctx;
7663
9154
  const blocks = [];
7664
9155
  if (access.insert) {
7665
9156
  const pinnedSet = new Set(pinned);
7666
- const insertable = table.columns.filter(
9157
+ const insertable = clientWritableColumns(table).filter(
7667
9158
  (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
7668
9159
  );
7669
9160
  if (insertable.length === 0) {
@@ -7756,6 +9247,10 @@ function renderMutationRoot(tables, namesByTable) {
7756
9247
  if (!canInsert && !canUpdate) continue;
7757
9248
  const names = namesByTable.get(table.table);
7758
9249
  if (!names) continue;
9250
+ if (table.resolved !== void 0) {
9251
+ lines.push(...renderResolvedMutationFields(table, names));
9252
+ continue;
9253
+ }
7759
9254
  const constraints = constraintValues(table);
7760
9255
  const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
7761
9256
  const updateArgs = [`_set: ${names.setInput}`];
@@ -7844,13 +9339,65 @@ function renderOrmMigrationExport(exported) {
7844
9339
  return text4;
7845
9340
  }
7846
9341
 
9342
+ // src/compiler-gql/role-warnings.ts
9343
+ var ROLE_CHECK = /\brole\(\s*['"]([^'"]+)['"]\s*\)/g;
9344
+ function isRecord(value) {
9345
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9346
+ }
9347
+ function roleKeysIn(value) {
9348
+ const keys = /* @__PURE__ */ new Set();
9349
+ for (const match of JSON.stringify(value ?? null).matchAll(ROLE_CHECK)) {
9350
+ if (match[1]) keys.add(match[1]);
9351
+ }
9352
+ return [...keys].sort();
9353
+ }
9354
+ function roleWarnings({
9355
+ canonical,
9356
+ manifest
9357
+ }) {
9358
+ const warnings = [];
9359
+ const roles = canonical.capabilities.roles;
9360
+ const defined = isRecord(roles) && isRecord(roles.definitions) ? Object.keys(roles.definitions).sort() : [];
9361
+ if (defined.length > 0) {
9362
+ warnings.push({
9363
+ severity: "warning",
9364
+ code: "deprecated-role-definitions",
9365
+ message: `The namespace defines roles (${defined.join(", ")}). Role definitions here are a deprecated one-time seed: apply creates any that don't exist yet and never updates them after. Create and edit roles in Atlas; keep declaring the capabilities your policies check.`
9366
+ });
9367
+ }
9368
+ const relations = [
9369
+ ...canonical.tables,
9370
+ ...(canonical.views ?? []).map((view) => ({ table: view.view, policies: view.policies, resolved: view.resolved }))
9371
+ ];
9372
+ for (const relation of relations) {
9373
+ const keys = roleKeysIn([relation.policies, relation.resolved]);
9374
+ if (keys.length === 0) continue;
9375
+ warnings.push({
9376
+ severity: "warning",
9377
+ code: "role-check",
9378
+ table: relation.table,
9379
+ message: `Policy checks ${keys.map((key) => `role('${key}')`).join(", ")}. Roles are defined in Atlas and can be renamed or removed there; check the capability the role grants with capability('\u2026') instead.`
9380
+ });
9381
+ }
9382
+ for (const [operation, entry] of Object.entries(manifest.operations).sort(([a], [b]) => a.localeCompare(b))) {
9383
+ if (!entry.requires?.role) continue;
9384
+ warnings.push({
9385
+ severity: "warning",
9386
+ code: "role-requires",
9387
+ operation,
9388
+ message: `@requires(role: "${entry.requires.role}") gates on a role Atlas can rename or remove; use @requires(capability: "\u2026") with the capability the role grants.`
9389
+ });
9390
+ }
9391
+ return warnings;
9392
+ }
9393
+
7847
9394
  // src/cli.ts
7848
9395
  var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
7849
9396
  var TOKEN_ENV = "SEQUENCE_ORM_TOKEN";
7850
9397
  var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
7851
9398
  var USAGE = `usage:
7852
9399
  seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
7853
- seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (+ consumer graphql-codegen when codegen.ts is present)
9400
+ seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (+ sources.gen.ts for resolved-table sources, + consumer graphql-codegen when codegen.ts is present)
7854
9401
  seq-studio orm generate [dir] --migration-export [--check]
7855
9402
  export reviewed ORM inputs for a coordinated pipeline migration; --check verifies without writing
7856
9403
  seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
@@ -8021,6 +9568,7 @@ async function walkFiles(root, prefix = "") {
8021
9568
  var SCHEMA_GRAPHQL_FILE = "schema.graphql";
8022
9569
  var OPERATIONS_MANIFEST_FILE = "operations.manifest.json";
8023
9570
  var TYPE_POLICIES_FILE = "typePolicies.gen.ts";
9571
+ var SOURCE_CONTRACTS_FILE = "sources.gen.ts";
8024
9572
  var GRAPHQL_DIRNAME = "graphql";
8025
9573
  var CODEGEN_CONFIG_FILE = "codegen.ts";
8026
9574
  function ormLoadOptions() {
@@ -8040,9 +9588,10 @@ async function compileV2Project(dir) {
8040
9588
  const documents = await collectGraphqlDocuments(dir);
8041
9589
  const result = compileNamespace({ canonical: loaded.canonical, sdl, documents });
8042
9590
  if (!result.ok) return { ok: false, diagnostics: result.diagnostics };
9591
+ const diagnostics = [...result.diagnostics, ...roleWarnings({ canonical: loaded.canonical, manifest: result.manifest })];
8043
9592
  return {
8044
9593
  ok: true,
8045
- project: { loaded, sdl, documents, manifest: result.manifest, diagnostics: result.diagnostics }
9594
+ project: { loaded, sdl, documents, manifest: result.manifest, diagnostics }
8046
9595
  };
8047
9596
  }
8048
9597
  var GQL_TEMPLATE_RE = /(?:\bgql|\/\*\s*GraphQL\s*\*\/)\s*`([^`]*)`/g;
@@ -8146,6 +9695,8 @@ async function writeGenerateOutputs(dir, project) {
8146
9695
  await promises.writeFile(path.join(dir, OPERATIONS_MANIFEST_FILE), `${JSON.stringify(manifestFile, null, 2)}
8147
9696
  `);
8148
9697
  await promises.writeFile(path.join(dir, TYPE_POLICIES_FILE), generateTypePolicies(loaded.canonical));
9698
+ const sourceContracts = generateSourceContracts(loaded.canonical);
9699
+ if (sourceContracts !== null) await promises.writeFile(path.join(dir, SOURCE_CONTRACTS_FILE), sourceContracts);
8149
9700
  }
8150
9701
  async function runConsumerCodegen(dir) {
8151
9702
  const codegenPath = path.join(dir, CODEGEN_CONFIG_FILE);
@@ -8413,16 +9964,28 @@ async function checkWorkspaceV2({
8413
9964
  return 1;
8414
9965
  }
8415
9966
  const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
8416
- const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
8417
- const regenerated = renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan });
8418
9967
  const committed = await readMigrationSql(dir, tip);
8419
- if (regenerated !== committed && !matchesRebaseTip({ committed, tip, namespace, plan })) {
9968
+ const { reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
9969
+ if (!reproduced) {
8420
9970
  log(`${MIGRATIONS_DIRNAME}/${tip.name}.sql does not regenerate from the definitions \u2014 re-run \`seq-studio orm diff\``);
8421
9971
  return 1;
8422
9972
  }
8423
9973
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
8424
9974
  return 0;
8425
9975
  }
9976
+ function planCommittedTipV2({
9977
+ loaded,
9978
+ tip,
9979
+ previousSnapshot,
9980
+ committed
9981
+ }) {
9982
+ const namespace = loaded.canonical.namespace;
9983
+ return planCommittedMigrationV2({
9984
+ canonical: loaded.canonical,
9985
+ previousSnapshot,
9986
+ reproduces: (plan) => renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan }) === committed || matchesRebaseTip({ committed, tip, namespace, plan })
9987
+ });
9988
+ }
8426
9989
  function matchesRebaseTip({
8427
9990
  committed,
8428
9991
  tip,
@@ -8525,7 +10088,6 @@ async function loadCommittedTipMigrationV2({
8525
10088
  loaded,
8526
10089
  workspace
8527
10090
  }) {
8528
- const namespace = loaded.canonical.namespace;
8529
10091
  const tip = workspace.journal.entries.at(-1);
8530
10092
  if (!tip) return void 0;
8531
10093
  if (tip.content_hash !== loaded.hash) {
@@ -8535,10 +10097,9 @@ async function loadCommittedTipMigrationV2({
8535
10097
  );
8536
10098
  }
8537
10099
  const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
8538
- const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
8539
10100
  const committed = await readMigrationSql(dir, tip);
8540
- const regenerated = renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan });
8541
- if (regenerated !== committed && !matchesRebaseTip({ committed, tip, namespace, plan })) {
10101
+ const { plan, reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
10102
+ if (!reproduced) {
8542
10103
  throw new OrmDefinitionError(
8543
10104
  `${tip.name}.sql does not regenerate from the definitions \u2014 run \`seq-studio orm diff --check\``,
8544
10105
  MIGRATIONS_DIRNAME