@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/compiler.cjs CHANGED
@@ -8,9 +8,25 @@ var graphql = require('graphql');
8
8
 
9
9
  // src/constants.ts
10
10
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
11
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
11
+ var MAX_IDENTIFIER_LENGTH = 63;
12
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
12
13
  var ORM_SCHEMA_PREFIX = "orm_";
13
14
  var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
15
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
16
+ var CELL_PART_SUFFIX = {
17
+ at: "__at",
18
+ by: "__by",
19
+ rev: "__rev",
20
+ source: "__src",
21
+ conflicts: "__lost"
22
+ };
23
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
24
+ var CELLS_FIELD = "cells";
25
+ function cellColumnName(column, part) {
26
+ return `${column}${CELL_PART_SUFFIX[part]}`;
27
+ }
28
+ var MATERIALIZE_ACTOR = "orm:materialize";
29
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
14
30
 
15
31
  // src/hash.ts
16
32
  function stableJson(value) {
@@ -19,9 +35,468 @@ function stableJson(value) {
19
35
  const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
20
36
  return `{${entries.join(",")}}`;
21
37
  }
38
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
39
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
40
+
41
+ // src/checks/compile.ts
42
+ function tokenize(source) {
43
+ const tokens = [];
44
+ for (let position = 0; position < source.length; ) {
45
+ const char = source[position];
46
+ if (/\s/.test(char)) {
47
+ position++;
48
+ continue;
49
+ }
50
+ if (char === "(" || char === ")" || char === ",") {
51
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
52
+ position++;
53
+ continue;
54
+ }
55
+ if (char === "'") {
56
+ const start = position++;
57
+ let value = "";
58
+ let closed = false;
59
+ while (position < source.length) {
60
+ if (source[position] !== "'") {
61
+ value += source[position++];
62
+ continue;
63
+ }
64
+ if (source[position + 1] === "'") {
65
+ value += "'";
66
+ position += 2;
67
+ continue;
68
+ }
69
+ position++;
70
+ closed = true;
71
+ break;
72
+ }
73
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
74
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
75
+ tokens.push({ kind: "string", value, position: start });
76
+ continue;
77
+ }
78
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
79
+ if (operator) {
80
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
81
+ position += operator[0].length;
82
+ continue;
83
+ }
84
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
85
+ if (number) {
86
+ tokens.push({ kind: "number", value: number[0], position });
87
+ position += number[0].length;
88
+ continue;
89
+ }
90
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
91
+ if (identifier3) {
92
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
93
+ position += identifier3[0].length;
94
+ continue;
95
+ }
96
+ throw new Error(`unexpected character '${char}' at position ${position}`);
97
+ }
98
+ tokens.push({ kind: "end", value: "", position: source.length });
99
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
100
+ return tokens;
101
+ }
102
+ var Parser = class {
103
+ constructor(tokens, columns) {
104
+ this.tokens = tokens;
105
+ this.columns = /* @__PURE__ */ new Map();
106
+ for (const column of columns) {
107
+ for (const alias of [column.name, column.columnName]) {
108
+ const previous = this.columns.get(alias);
109
+ if (previous && previous.columnName !== column.columnName) {
110
+ throw new Error(`ambiguous check column '${alias}'`);
111
+ }
112
+ this.columns.set(alias, column);
113
+ }
114
+ }
115
+ }
116
+ index = 0;
117
+ depth = 0;
118
+ columns;
119
+ parse() {
120
+ const expression = this.parseOr();
121
+ this.expect("end", "end of expression");
122
+ return expression;
123
+ }
124
+ parseOr() {
125
+ let left = this.parseAnd();
126
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
127
+ return left;
128
+ }
129
+ parseAnd() {
130
+ let left = this.parseNot();
131
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
132
+ return left;
133
+ }
134
+ parseNot() {
135
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
136
+ return this.parsePrimary();
137
+ }
138
+ parsePrimary() {
139
+ if (this.peek().kind === "lparen") {
140
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
141
+ this.advance();
142
+ const expression = this.parseOr();
143
+ this.expect("rparen", "')'");
144
+ this.depth--;
145
+ return `(${expression})`;
146
+ }
147
+ return this.parseComparison();
148
+ }
149
+ parseComparison() {
150
+ const left = this.parseValue();
151
+ if (this.matchKeyword("is")) {
152
+ const negate = this.matchKeyword("not");
153
+ this.expectKeyword("null");
154
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
155
+ }
156
+ if (this.matchKeyword("between")) {
157
+ const lower = this.parseValue();
158
+ this.expectKeyword("and");
159
+ const upper = this.parseValue();
160
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
161
+ }
162
+ if (this.matchKeyword("in")) {
163
+ this.expect("lparen", "'(' after IN");
164
+ const values = [this.parseValue()];
165
+ while (this.peek().kind === "comma") {
166
+ this.advance();
167
+ values.push(this.parseValue());
168
+ }
169
+ this.expect("rparen", "')' after IN values");
170
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
171
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
172
+ }
173
+ const operator = this.expect("operator", "a comparison operator");
174
+ const right = this.parseValue();
175
+ if (operator.value === "~" || operator.value === "!~") {
176
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
177
+ if (!right.literal) throw new Error("regex pattern must be a literal");
178
+ }
179
+ return `${left.sql} ${operator.value} ${right.sql}`;
180
+ }
181
+ parseValue() {
182
+ const token = this.advance();
183
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
184
+ if (token.kind === "number") {
185
+ const number = Number(token.value);
186
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
187
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
188
+ }
189
+ return { sql: token.value, literal: true };
190
+ }
191
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
192
+ const keyword = token.value.toLowerCase();
193
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
194
+ if (keyword === "btrim") {
195
+ this.expect("lparen", "'(' after btrim");
196
+ const column2 = this.parseColumn();
197
+ this.expect("rparen", "')' after btrim column");
198
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
199
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
200
+ }
201
+ const column = this.columns.get(token.value);
202
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
203
+ return { sql: quoteIdentifier(column.columnName), column };
204
+ }
205
+ parseColumn() {
206
+ const token = this.expect("identifier", "a column name");
207
+ const column = this.columns.get(token.value);
208
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
209
+ return column;
210
+ }
211
+ peek() {
212
+ return this.tokens[this.index];
213
+ }
214
+ advance() {
215
+ const token = this.peek();
216
+ if (token.kind !== "end") this.index++;
217
+ return token;
218
+ }
219
+ matchKeyword(keyword) {
220
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
221
+ this.advance();
222
+ return true;
223
+ }
224
+ expectKeyword(keyword) {
225
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
226
+ }
227
+ expect(kind, description) {
228
+ const token = this.peek();
229
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
230
+ return this.advance();
231
+ }
232
+ };
233
+ function compileCheckExpression(source, columns) {
234
+ return new Parser(tokenize(source), columns).parse();
235
+ }
236
+ var policyVariantSchema = zod.z.object({
237
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
238
+ when: zod.z.string().min(1).max(1e3),
239
+ /** Exactly one of: allow-all bypass, or a row predicate. */
240
+ allow: zod.z.literal(true).optional(),
241
+ policy: zod.z.string().min(1).max(2e3).optional()
242
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
243
+ message: "A variant declares exactly one of allow: true or policy"
244
+ });
245
+ var policyExpressionSchema = zod.z.union([
246
+ zod.z.string().min(1).max(2e3),
247
+ zod.z.object({
248
+ default: zod.z.string().min(1).max(2e3),
249
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
250
+ })
251
+ ]);
252
+ function parseDurationMs(input) {
253
+ if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
254
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
255
+ if (!match) return void 0;
256
+ const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
257
+ const value = Math.round(Number(match[1]) * factor);
258
+ return value > 0 ? value : void 0;
259
+ }
260
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
261
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
262
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
263
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
264
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
265
+ };
266
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
267
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
268
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
269
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
270
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
271
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
272
+ };
273
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
274
+ /* @__PURE__ */ new Set([
275
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
276
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
277
+ ]);
278
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
279
+ var canonicalFunctionRefSchema = zod.z.object({
280
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
281
+ kind: zod.z.enum(["managed", "native"]),
282
+ id: zod.z.string().min(1).max(200)
283
+ }).strict();
284
+ var canonicalFetchShapeSchema = zod.z.union([
285
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
286
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
287
+ ]);
288
+ var canonicalResolvedSourceSchema = zod.z.object({
289
+ name: identifier,
290
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
291
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
292
+ table: identifier,
293
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
294
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
295
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
296
+ updatedAt: zod.z.string().min(1).max(63).optional(),
297
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
298
+ key: zod.z.string().min(1).max(63).optional(),
299
+ fetch: canonicalFetchShapeSchema.optional(),
300
+ function: canonicalFunctionRefSchema.optional(),
301
+ prefetchDefaults: zod.z.object({
302
+ maxStalenessMs: durationMs,
303
+ timeoutMs: durationMs,
304
+ onError: zod.z.enum(["fallback", "fail"])
305
+ }).strict().optional(),
306
+ writeback: zod.z.object({
307
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
308
+ defaultMode: zod.z.enum(["sync", "async"]),
309
+ timeoutMs: durationMs,
310
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
311
+ clearManualOnConfirm: zod.z.boolean()
312
+ }).strict().optional()
313
+ }).strict();
314
+ var canonicalResolveRuleSchema = zod.z.object({
315
+ column: zod.z.string().min(1).max(63),
316
+ comparator: zod.z.enum(COMPARATORS),
317
+ /** Source names, in tie-break order. */
318
+ candidates: zod.z.array(identifier).min(1).max(16),
319
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
320
+ orderBy: zod.z.string().min(1).max(2e3).optional()
321
+ }).strict();
322
+ var canonicalResolvedSchema = zod.z.object({
323
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
324
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
325
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
326
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
327
+ deletedBy: zod.z.array(identifier).max(1).default([]),
328
+ /**
329
+ * Materialized: the view reads an indexed internal table (`table`) that the
330
+ * data plane keeps in step with `compute`, the internal view holding the
331
+ * resolution query. Omitted when the view computes on read.
332
+ */
333
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
334
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
335
+ policies: zod.z.object({
336
+ write: policyExpressionSchema.optional(),
337
+ insert: policyExpressionSchema.optional(),
338
+ update: policyExpressionSchema.optional()
339
+ }).strict().default({})
340
+ }).strict();
341
+ function validateResolvedView({ view, tables, views = [] }) {
342
+ const resolved = view.resolved;
343
+ if (!resolved) return [];
344
+ const errors = [];
345
+ const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
346
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
347
+ if (!pkColumn) {
348
+ fail3("requires exactly one primary-key column");
349
+ return errors;
350
+ }
351
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
352
+ const scope = {
353
+ view,
354
+ resolved,
355
+ pkColumn,
356
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
357
+ sourceByName: /* @__PURE__ */ new Map(),
358
+ fail: fail3
359
+ };
360
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail3("requires exactly one manual source");
361
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
362
+ for (const source of resolved.sources) {
363
+ if (scope.sourceByName.has(source.name)) fail3(`duplicate source '${source.name}'`);
364
+ scope.sourceByName.set(source.name, source);
365
+ const table = tableByName.get(source.table);
366
+ if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
367
+ else validateSource({ scope, source, table });
368
+ }
369
+ validateRules(scope);
370
+ validateViewCells(scope);
371
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
372
+ for (const name of resolved.deletedBy) {
373
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail3(`deletedBy '${name}' must be the manual source`);
374
+ }
375
+ return errors;
376
+ }
377
+ function validateSource({ scope, source, table }) {
378
+ const { fail: fail3 } = scope;
379
+ if (source.kind === "etl" && table.source === void 0) fail3(`etl source '${source.name}' must reference a synced table`);
380
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
381
+ fail3(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
382
+ }
383
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
384
+ if (source.kind === "manual" && !hasRev) fail3(`manual source '${source.name}' must carry '__rev' cell columns`);
385
+ if (source.kind === "prefetch" && hasRev) fail3(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
386
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
387
+ validateSourceColumns({ scope, source, byName });
388
+ if (source.kind === "etl") {
389
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
390
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
391
+ fail3(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
392
+ }
393
+ }
394
+ validatePrefetchFields({ scope, source });
395
+ }
396
+ function validatePrefetchFields({ scope, source }) {
397
+ const { fail: fail3, pkColumn } = scope;
398
+ if (source.kind !== "prefetch") {
399
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
400
+ fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
401
+ }
402
+ return;
403
+ }
404
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
405
+ fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
406
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
407
+ fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
408
+ }
409
+ for (const column of source.writeback?.columns ?? []) {
410
+ if (!source.columns.includes(column)) fail3(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
411
+ }
412
+ }
413
+ function validateSourceColumns({ scope, source, byName }) {
414
+ const { fail: fail3, pkColumn, dataColumns } = scope;
415
+ for (const column of [pkColumn.name, ...source.columns]) {
416
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
417
+ const theirs = byName.get(column);
418
+ if (!own) fail3(`source '${source.name}' supplies unknown column '${column}'`);
419
+ else if (!theirs) fail3(`source '${source.name}' table '${source.table}' has no column '${column}'`);
420
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail3(`column '${column}' has a different type on source '${source.name}'`);
421
+ }
422
+ }
423
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail3 }) {
424
+ const ruled = /* @__PURE__ */ new Set();
425
+ for (const rule of resolved.resolve) {
426
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
427
+ fail3(`resolve rule targets unknown column '${rule.column}'`);
428
+ continue;
429
+ }
430
+ if (ruled.has(rule.column)) fail3(`column '${rule.column}' has more than one resolve rule`);
431
+ ruled.add(rule.column);
432
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
433
+ fail3(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
434
+ }
435
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail3(`column '${rule.column}' repeats a candidate`);
436
+ for (const candidate of rule.candidates) {
437
+ const source = sourceByName.get(candidate);
438
+ if (!source) fail3(`column '${rule.column}' names unknown candidate source '${candidate}'`);
439
+ else if (!source.columns.includes(rule.column)) fail3(`column '${rule.column}' candidate '${candidate}' does not supply it`);
440
+ }
441
+ }
442
+ for (const column of dataColumns.keys()) {
443
+ if (column !== pkColumn.name && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
444
+ }
445
+ }
446
+ var VIEW_CELL_TYPES = {
447
+ source: "text",
448
+ at: "timestamptz",
449
+ by: "text",
450
+ conflicts: "jsonb"
451
+ };
452
+ function validateViewCells({ view, dataColumns, pkColumn, fail: fail3 }) {
453
+ const expected = /* @__PURE__ */ new Map();
454
+ for (const column of dataColumns.values()) {
455
+ if (column.name === pkColumn.name) continue;
456
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
457
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
458
+ }
459
+ }
460
+ for (const column of view.columns) {
461
+ if (!column.cell) continue;
462
+ const want = expected.get(column.name);
463
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
464
+ fail3(`unexpected cell column '${column.name}'`);
465
+ continue;
466
+ }
467
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail3(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
468
+ expected.delete(column.name);
469
+ }
470
+ for (const name of expected.keys()) fail3(`missing cell column '${name}'`);
471
+ }
472
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
473
+ function validateMaterialized({ scope, tableByName, views }) {
474
+ const { view, resolved, fail: fail3 } = scope;
475
+ const { table: tableName, compute: computeName } = resolved.materialized;
476
+ const table = tableByName.get(tableName);
477
+ if (!table || table.internal !== true || table.materializes !== view.view) {
478
+ fail3(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
479
+ } else {
480
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail3(`materialized table '${tableName}' must have exactly the view's columns`);
481
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail3(`materialized table '${tableName}' must share the view's primary key`);
482
+ }
483
+ const compute = views.find((candidate) => candidate.view === computeName);
484
+ if (!compute || compute.internal !== true) fail3(`compute view '${computeName}' must be an internal view`);
485
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail3(`compute view '${computeName}' must have exactly the view's columns`);
486
+ for (const source of resolved.sources) {
487
+ if (source.kind !== "etl") continue;
488
+ const read = tableByName.get(source.table)?.policies?.read;
489
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
490
+ if (!unconditional) {
491
+ fail3(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
492
+ }
493
+ }
494
+ }
495
+
496
+ // src/canonical/v2.ts
22
497
  var CANONICAL_FORMAT = 2;
23
498
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
24
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
499
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
25
500
  var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
26
501
  var COLUMN_TYPES = [
27
502
  "uuid",
@@ -37,14 +512,14 @@ var COLUMN_TYPES = [
37
512
  ];
38
513
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
39
514
  var canonicalEnumSchema = zod.z.object({
40
- name: identifier,
515
+ name: identifier2,
41
516
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
42
517
  });
43
518
  var canonicalColumnSchema = zod.z.object({
44
519
  /** Property name in TS (camelCase); columnName is the Postgres name. */
45
520
  name: zod.z.string().min(1).max(63),
46
521
  columnName: columnIdentifier,
47
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
522
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
48
523
  nullable: zod.z.boolean().default(true),
49
524
  unique: zod.z.boolean().default(false),
50
525
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -60,7 +535,14 @@ var canonicalColumnSchema = zod.z.object({
60
535
  /** Migration hints — same semantics as v1. */
61
536
  renamedFrom: columnIdentifier.optional(),
62
537
  cast: zod.z.string().max(200).optional(),
63
- deprecated: zod.z.boolean().default(false)
538
+ deprecated: zod.z.boolean().default(false),
539
+ /**
540
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
541
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
542
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
543
+ * ordinary columns so their hashes are unchanged.
544
+ */
545
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
64
546
  }).superRefine((column, ctx) => {
65
547
  if (column.onUpdateFn === void 0) return;
66
548
  if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
@@ -75,36 +557,26 @@ var canonicalLinkSchema = zod.z.object({
75
557
  name: zod.z.string().min(1).max(63),
76
558
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
77
559
  /** Target table name (namespace-local). Absent for toPlatformUser. */
78
- target: identifier.optional(),
560
+ target: identifier2.optional(),
79
561
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
80
562
  on: columnIdentifier,
81
563
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
82
564
  references: columnIdentifier.optional(),
83
565
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
84
566
  enforce: zod.z.boolean().default(false),
567
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
568
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
85
569
  description: zod.z.string().max(500).optional()
86
570
  });
87
571
  var canonicalIndexSchema = zod.z.object({
88
572
  columns: zod.z.array(columnIdentifier).min(1),
89
573
  unique: zod.z.boolean().default(false),
90
- name: identifier.optional()
91
- });
92
- var policyVariantSchema = zod.z.object({
93
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
94
- when: zod.z.string().min(1).max(1e3),
95
- /** Exactly one of: allow-all bypass, or a row predicate. */
96
- allow: zod.z.literal(true).optional(),
97
- policy: zod.z.string().min(1).max(2e3).optional()
98
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
99
- message: "A variant declares exactly one of allow: true or policy"
574
+ name: identifier2.optional()
100
575
  });
101
- var policyExpressionSchema = zod.z.union([
102
- zod.z.string().min(1).max(2e3),
103
- zod.z.object({
104
- default: zod.z.string().min(1).max(2e3),
105
- variants: zod.z.array(policyVariantSchema).max(10).default([])
106
- })
107
- ]);
576
+ var canonicalCheckSchema = zod.z.object({
577
+ name: identifier2,
578
+ expression: zod.z.string().min(1).max(2e3)
579
+ }).strict();
108
580
  var canonicalPoliciesSchema = zod.z.object({
109
581
  read: policyExpressionSchema.optional(),
110
582
  write: policyExpressionSchema.optional(),
@@ -135,7 +607,7 @@ function isSingleViewQuerySql(value) {
135
607
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
136
608
  }
137
609
  var canonicalActionSchema = zod.z.object({
138
- name: identifier,
610
+ name: identifier2,
139
611
  requires: zod.z.string().max(1e3).optional(),
140
612
  params: zod.z.record(zod.z.string(), zod.z.object({
141
613
  type: columnTypeSchema,
@@ -147,9 +619,9 @@ var canonicalActionSchema = zod.z.object({
147
619
  });
148
620
  var canonicalCowMutationSchema = zod.z.object({
149
621
  kind: zod.z.enum(["override", "revert"]),
150
- name: identifier,
622
+ name: identifier2,
151
623
  /** Synced table (same namespace) the overlay copies from / falls back to. */
152
- synced: identifier,
624
+ synced: identifier2,
153
625
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
154
626
  pk: zod.z.string().min(1).max(63),
155
627
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -177,23 +649,27 @@ var canonicalCowMutationSchema = zod.z.object({
177
649
  }
178
650
  });
179
651
  var canonicalForeignKeySchema = zod.z.object({
180
- name: identifier,
652
+ name: identifier2,
181
653
  columns: zod.z.array(columnIdentifier).min(1).max(8),
182
- target: identifier,
654
+ target: identifier2,
183
655
  references: zod.z.array(columnIdentifier).min(1).max(8),
184
- deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
656
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
657
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
658
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
185
659
  }).strict();
186
660
  var canonicalTableSchema = zod.z.object({
187
- table: identifier,
661
+ table: identifier2,
188
662
  description: zod.z.string().max(2e3).optional(),
189
663
  /** Table-level rename hint (new in v2). */
190
- renamedFrom: identifier.optional(),
664
+ renamedFrom: identifier2.optional(),
191
665
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
192
666
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
193
667
  columns: zod.z.array(canonicalColumnSchema).default([]),
194
668
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
195
669
  links: zod.z.array(canonicalLinkSchema).default([]),
196
670
  indexes: zod.z.array(canonicalIndexSchema).default([]),
671
+ /** Omitted by old definitions, preserving their existing content hashes. */
672
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
197
673
  policies: canonicalPoliciesSchema.default({}),
198
674
  /**
199
675
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -204,22 +680,55 @@ var canonicalTableSchema = zod.z.object({
204
680
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
205
681
  cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
206
682
  /** Omitted by old definitions, preserving their existing content hashes. */
207
- foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
683
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
684
+ /**
685
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
686
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
687
+ * on a resolved table's internal sources), listed explicitly in `columns`
688
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
689
+ * Omitted when off so existing definitions hash unchanged.
690
+ */
691
+ cellMetadata: zod.z.literal(true).optional(),
692
+ /**
693
+ * System-owned backing table of a resolved table (its manual or prefetch
694
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
695
+ * only the data plane writes them. Omitted when false.
696
+ */
697
+ internal: zod.z.literal(true).optional(),
698
+ /**
699
+ * The materialized copy of resolved table `materializes` (internal): the
700
+ * same columns as that view, including its cell parts, maintained by the
701
+ * data plane. Omitted otherwise.
702
+ */
703
+ materializes: identifier2.optional()
208
704
  });
705
+ var MAX_VIEW_QUERY = 1e5;
706
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
209
707
  var canonicalViewSchema = zod.z.object({
210
- view: identifier,
708
+ view: identifier2,
211
709
  description: zod.z.string().max(2e3).optional(),
212
710
  columns: zod.z.array(canonicalColumnSchema).min(1),
213
711
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
214
712
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
215
713
  links: zod.z.array(canonicalLinkSchema).default([]),
216
714
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
217
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
715
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
218
716
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
219
717
  }),
220
718
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
221
- dependencies: zod.z.array(identifier).max(500)
719
+ dependencies: zod.z.array(identifier2).max(500),
720
+ /**
721
+ * Present when this view is a resolved table (defineResolvedTable): the
722
+ * multi-source metadata behind its generated query. Omitted otherwise so
723
+ * existing views hash unchanged.
724
+ */
725
+ resolved: canonicalResolvedSchema.optional(),
726
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
727
+ internal: zod.z.literal(true).optional()
222
728
  }).superRefine((view, ctx) => {
729
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
730
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
731
+ }
223
732
  for (const [index, column] of view.columns.entries()) {
224
733
  if (column.default !== void 0 || column.defaultFn !== void 0) {
225
734
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
@@ -234,15 +743,82 @@ var canonicalViewSchema = zod.z.object({
234
743
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
235
744
  }
236
745
  }
746
+ if (view.resolved === void 0 && view.internal !== true) {
747
+ for (const [index, column] of view.columns.entries()) {
748
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
749
+ }
750
+ }
237
751
  for (const [index, link] of view.links.entries()) {
238
752
  if (link.enforce) {
239
753
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
240
754
  }
755
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
756
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
757
+ }
241
758
  }
242
759
  });
760
+ var CELL_PART_TYPES = {
761
+ at: "timestamptz",
762
+ by: "text",
763
+ rev: "uuid",
764
+ source: "text",
765
+ conflicts: "jsonb"
766
+ };
767
+ function trackedCellColumns(table) {
768
+ const pk = new Set(table.primaryKey ?? []);
769
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
770
+ }
771
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
772
+ function validateCellMetadata(table, index, ctx) {
773
+ const fail3 = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
774
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
775
+ if (table.materializes !== void 0) return;
776
+ if (table.cellMetadata !== true) {
777
+ for (const { position } of cells) fail3(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
778
+ return;
779
+ }
780
+ if (table.source !== void 0) {
781
+ fail3(`synced table '${table.table}' cannot track cell metadata`);
782
+ return;
783
+ }
784
+ const tracked = trackedCellColumns(table);
785
+ const byName = new Map(tracked.map((column) => [column.name, column]));
786
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
787
+ if (withRev && table.internal !== true) fail3(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
788
+ const seen = /* @__PURE__ */ new Set();
789
+ for (const { column, position, cell } of cells) {
790
+ const path = ["tables", index, "columns", position];
791
+ const owner = byName.get(cell.of);
792
+ if (!owner) {
793
+ fail3(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
794
+ continue;
795
+ }
796
+ 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);
797
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
798
+ fail3(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
799
+ }
800
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
801
+ fail3(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
802
+ }
803
+ const key = `${cell.of}:${cell.part}`;
804
+ if (seen.has(key)) fail3(`duplicate cell column '${column.name}'`, path);
805
+ seen.add(key);
806
+ }
807
+ for (const column of tracked) {
808
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
809
+ fail3(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
810
+ }
811
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
812
+ fail3(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
813
+ }
814
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
815
+ if (!seen.has(`${column.name}:${part}`)) fail3(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
816
+ }
817
+ }
818
+ }
243
819
  var canonicalDefinitionSchema = zod.z.object({
244
820
  definition_format: zod.z.literal(CANONICAL_FORMAT),
245
- namespace: identifier.max(41),
821
+ namespace: identifier2.max(41),
246
822
  description: zod.z.string().max(2e3).optional(),
247
823
  enums: zod.z.array(canonicalEnumSchema).default([]),
248
824
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
@@ -260,6 +836,7 @@ var canonicalDefinitionSchema = zod.z.object({
260
836
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
261
837
  }
262
838
  seen.add(table.table);
839
+ validateCellMetadata(table, index, ctx);
263
840
  if (table.source !== void 0) {
264
841
  for (const [columnIndex, column] of table.columns.entries()) {
265
842
  if (column.onUpdateFn !== void 0) {
@@ -279,25 +856,53 @@ var canonicalDefinitionSchema = zod.z.object({
279
856
  ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
280
857
  ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
281
858
  ]);
859
+ for (const [index, link] of table.links.entries()) {
860
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
861
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
862
+ }
863
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
864
+ const source = table.columns.find((column) => column.columnName === link.on);
865
+ if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "SET NULL requires a nullable source column" });
866
+ }
867
+ }
868
+ for (const [index, check] of (table.checks ?? []).entries()) {
869
+ const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
870
+ if (table.source) fail3("synced tables cannot declare checks");
871
+ if (names.has(check.name)) fail3(`duplicate constraint '${check.name}'`);
872
+ names.add(check.name);
873
+ try {
874
+ compileCheckExpression(check.expression, table.columns);
875
+ } catch (error) {
876
+ fail3(error instanceof Error ? error.message : String(error));
877
+ }
878
+ }
282
879
  for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
283
- const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
284
- if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
880
+ const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
881
+ if (names.has(fk.name)) fail3(`duplicate foreign key '${fk.name}'`);
285
882
  names.add(fk.name);
286
883
  const target = fkTables.get(fk.target);
287
884
  if (table.source || !target || target.source) {
288
- fail2("foreign keys must connect owned tables in the same namespace");
885
+ fail3("foreign keys must connect owned tables in the same namespace");
289
886
  continue;
290
887
  }
291
- if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
292
- if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
888
+ if (fk.columns.length !== fk.references.length) fail3("foreign key columns and references must have the same number of columns");
889
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail3("foreign key columns must not repeat");
293
890
  for (const [position, name] of fk.columns.entries()) {
294
891
  const sourceColumn = table.columns.find((column) => column.columnName === name);
295
892
  const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
296
- if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
297
- else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
893
+ if (!sourceColumn || !targetColumn) fail3("foreign key references an unknown column");
894
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail3("foreign key column types must match");
895
+ }
896
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
897
+ 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");
298
898
  }
299
899
  const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
300
- 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");
900
+ 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");
901
+ }
902
+ }
903
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
904
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
905
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
301
906
  }
302
907
  }
303
908
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
@@ -423,8 +1028,12 @@ function canonicalRelations(definition) {
423
1028
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
424
1029
  links: view.links,
425
1030
  indexes: [],
426
- policies: { read: view.policies.read },
427
- actions: []
1031
+ // A resolved table's write policies (evaluated against the resolved row)
1032
+ // ride alongside the view's read policy.
1033
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
1034
+ actions: [],
1035
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
1036
+ ...view.internal ? { internal: true } : {}
428
1037
  }));
429
1038
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
430
1039
  }
@@ -434,13 +1043,14 @@ function normalizeCanonical(definition) {
434
1043
  ...definition,
435
1044
  enums: sortByName(definition.enums),
436
1045
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
437
- const { cowMutations, foreignKeys, ...rest } = t;
1046
+ const { cowMutations, foreignKeys, checks, ...rest } = t;
438
1047
  return {
439
1048
  ...rest,
440
1049
  links: sortByName(t.links),
441
1050
  indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
442
1051
  actions: sortByName(t.actions),
443
1052
  ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1053
+ ...checks?.length ? { checks: sortByName(checks) } : {},
444
1054
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
445
1055
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
446
1056
  };
@@ -540,6 +1150,21 @@ var selectionIrSchema = zod.z.lazy(
540
1150
  () => zod.z.object({
541
1151
  typename: zod.z.string(),
542
1152
  columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
1153
+ cells: zod.z.object({
1154
+ alias: zod.z.string(),
1155
+ typename: zod.z.string(),
1156
+ fields: zod.z.array(zod.z.object({
1157
+ alias: zod.z.string(),
1158
+ column: zod.z.string(),
1159
+ typename: zod.z.string(),
1160
+ parts: zod.z.array(zod.z.object({
1161
+ alias: zod.z.string(),
1162
+ part: zod.z.enum(["source", "at", "by", "conflicts"]),
1163
+ fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
1164
+ typename: zod.z.string().optional()
1165
+ }))
1166
+ }))
1167
+ }).optional(),
543
1168
  relations: zod.z.array(zod.z.object({
544
1169
  alias: zod.z.string(),
545
1170
  link: zod.z.string(),
@@ -561,6 +1186,11 @@ var selectionIrSchema = zod.z.lazy(
561
1186
  })
562
1187
  );
563
1188
  var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
1189
+ var prefetchPlanIrSchema = zod.z.object({
1190
+ source: zod.z.string(),
1191
+ maxStalenessMs: zod.z.number().int().positive().optional(),
1192
+ key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
1193
+ });
564
1194
  var selectRootSchema = zod.z.object({
565
1195
  kind: zod.z.literal("select"),
566
1196
  alias: zod.z.string(),
@@ -572,7 +1202,8 @@ var selectRootSchema = zod.z.object({
572
1202
  distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
573
1203
  limit: limitIrSchema,
574
1204
  offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
575
- includeIf: includeIfSchema.optional()
1205
+ includeIf: includeIfSchema.optional(),
1206
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
576
1207
  });
577
1208
  var byPkRootSchema = zod.z.object({
578
1209
  kind: zod.z.literal("byPk"),
@@ -582,7 +1213,8 @@ var byPkRootSchema = zod.z.object({
582
1213
  pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
583
1214
  /** Policy predicate — ANDed with the PK match. */
584
1215
  where: predIrSchema,
585
- includeIf: includeIfSchema.optional()
1216
+ includeIf: includeIfSchema.optional(),
1217
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
586
1218
  });
587
1219
  var aggregateRootSchema = zod.z.object({
588
1220
  kind: zod.z.literal("aggregate"),
@@ -640,7 +1272,20 @@ var mutationRootSchema = zod.z.object({
640
1272
  * coalesce view falls back to synced. Both are gated on a matching synced
641
1273
  * row and carry the write policy in `where` like update/delete.
642
1274
  */
643
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
1275
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
1276
+ /**
1277
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
1278
+ * executor gates on the resolved row (write policy in `where`), writes only
1279
+ * the touched cells into the manual source, then re-checks `insertCheck`
1280
+ * against the resolved row. `revert` clears `revertColumns` from the manual
1281
+ * source; `delete` tombstones the row there.
1282
+ */
1283
+ resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
1284
+ revertColumns: zod.z.array(zod.z.string()).optional(),
1285
+ /** `@writeback(source:, mode:)` directives on this root. */
1286
+ writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
1287
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
1288
+ localOnly: zod.z.array(zod.z.string()).optional(),
644
1289
  alias: zod.z.string(),
645
1290
  table: zod.z.string(),
646
1291
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -725,6 +1370,43 @@ var compiledOperationSchema = zod.z.object({
725
1370
  }
726
1371
  }
727
1372
  });
1373
+ var resolvedSourceIrSchema = zod.z.object({
1374
+ name: zod.z.string(),
1375
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
1376
+ /** Logical table key of the backing relation. */
1377
+ table: zod.z.string(),
1378
+ columns: zod.z.array(zod.z.string()),
1379
+ key: zod.z.string().optional(),
1380
+ fetch: zod.z.union([
1381
+ zod.z.object({ shape: zod.z.literal("single") }),
1382
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
1383
+ ]).optional(),
1384
+ function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
1385
+ prefetchDefaults: zod.z.object({
1386
+ maxStalenessMs: zod.z.number().int().positive(),
1387
+ timeoutMs: zod.z.number().int().positive(),
1388
+ onError: zod.z.enum(["fallback", "fail"])
1389
+ }).optional(),
1390
+ writeback: zod.z.object({
1391
+ columns: zod.z.array(zod.z.string()),
1392
+ defaultMode: zod.z.enum(["sync", "async"]),
1393
+ timeoutMs: zod.z.number().int().positive(),
1394
+ clearManualOnConfirm: zod.z.boolean()
1395
+ }).optional()
1396
+ });
1397
+ var resolvedMetaIrSchema = zod.z.object({
1398
+ /** Logical pk column of the resolved view (single-column). */
1399
+ pk: zod.z.string(),
1400
+ /** Source name of the manual source. */
1401
+ manual: zod.z.string(),
1402
+ sources: zod.z.array(resolvedSourceIrSchema),
1403
+ deletedBy: zod.z.array(zod.z.string()),
1404
+ /**
1405
+ * Materialized: the view reads `table`, which the data plane refreshes from
1406
+ * the `compute` view (claims-gated) whenever it writes a source row.
1407
+ */
1408
+ materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
1409
+ });
728
1410
  var tableMetaIrSchema = zod.z.object({
729
1411
  table: zod.z.string(),
730
1412
  schema: zod.z.string(),
@@ -765,6 +1447,22 @@ var tableMetaIrSchema = zod.z.object({
765
1447
  * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
766
1448
  */
767
1449
  touchOnUpdate: zod.z.array(zod.z.string()).optional(),
1450
+ /**
1451
+ * Per-cell metadata: logical column → the logical names of its cell-part
1452
+ * columns (which are listed in `columns`, but never filterable, orderable or
1453
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
1454
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
1455
+ * the relation tracks no cells.
1456
+ */
1457
+ cells: zod.z.record(zod.z.string(), zod.z.object({
1458
+ at: zod.z.string().optional(),
1459
+ by: zod.z.string().optional(),
1460
+ rev: zod.z.string().optional(),
1461
+ source: zod.z.string().optional(),
1462
+ conflicts: zod.z.string().optional()
1463
+ }).strict()).optional(),
1464
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
1465
+ resolved: resolvedMetaIrSchema.optional(),
768
1466
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
769
1467
  /** Depth/node caps for dynamic filter trees on this table. */
770
1468
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
@@ -797,6 +1495,115 @@ function normalizeIr(raw) {
797
1495
  return compiledArtifactSchema.parse(raw);
798
1496
  }
799
1497
 
1498
+ // src/sdl/naming.ts
1499
+ function camelCase(name) {
1500
+ const segments = name.split("_").filter((segment) => segment.length > 0);
1501
+ const first = segments[0];
1502
+ if (first === void 0) return name;
1503
+ const head = first.charAt(0).toLowerCase() + first.slice(1);
1504
+ return head + segments.slice(1).map(capitalizeFirst).join("");
1505
+ }
1506
+ function pascalCase(name) {
1507
+ return capitalizeFirst(camelCase(name));
1508
+ }
1509
+ function capitalizeFirst(segment) {
1510
+ return segment.charAt(0).toUpperCase() + segment.slice(1);
1511
+ }
1512
+ function singularizeWord(word) {
1513
+ if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
1514
+ for (const suffix of ["sses", "xes", "ches", "shes"]) {
1515
+ if (word.endsWith(suffix)) return word.slice(0, -2);
1516
+ }
1517
+ if (word.endsWith("ss")) return word;
1518
+ if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
1519
+ return word;
1520
+ }
1521
+ function typeName(table) {
1522
+ const segments = table.split("_").filter((segment) => segment.length > 0);
1523
+ const last = segments[segments.length - 1];
1524
+ if (last === void 0) return pascalCase(table);
1525
+ return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
1526
+ }
1527
+ function enumTypeName(name) {
1528
+ return pascalCase(name);
1529
+ }
1530
+ var SCALAR_FOR_COLUMN_TYPE = {
1531
+ uuid: "uuid",
1532
+ text: "String",
1533
+ integer: "Int",
1534
+ bigint: "bigint",
1535
+ numeric: "numeric",
1536
+ double: "Float",
1537
+ boolean: "Boolean",
1538
+ timestamptz: "timestamptz",
1539
+ date: "date",
1540
+ jsonb: "jsonb"
1541
+ };
1542
+ function graphqlScalarFor(type) {
1543
+ return SCALAR_FOR_COLUMN_TYPE[type];
1544
+ }
1545
+ var COLUMN_TYPE_BY_SCALAR = new Map([
1546
+ ["ID", "text"],
1547
+ ...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
1548
+ ]);
1549
+ function columnTypeForScalar(scalarName) {
1550
+ return COLUMN_TYPE_BY_SCALAR.get(scalarName);
1551
+ }
1552
+ var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
1553
+ var PLATFORM_USER_TYPENAME = "PlatformUser";
1554
+ function tableNames(table) {
1555
+ const root = camelCase(table);
1556
+ const rootPascal = pascalCase(table);
1557
+ const type = typeName(table);
1558
+ return {
1559
+ typename: type,
1560
+ selectRoot: root,
1561
+ byPkRoot: `${root}ByPk`,
1562
+ aggregateRoot: `${root}Aggregate`,
1563
+ insertRoot: `insert${rootPascal}`,
1564
+ insertOneRoot: `insert${rootPascal}One`,
1565
+ updateRoot: `update${rootPascal}`,
1566
+ updateByPkRoot: `update${rootPascal}ByPk`,
1567
+ deleteRoot: `delete${rootPascal}`,
1568
+ deleteByPkRoot: `delete${rootPascal}ByPk`,
1569
+ revertByPkRoot: `revert${rootPascal}ByPk`,
1570
+ boolExp: `${type}BoolExp`,
1571
+ orderBy: `${type}OrderBy`,
1572
+ selectColumn: `${type}SelectColumn`,
1573
+ insertInput: `${type}InsertInput`,
1574
+ setInput: `${type}SetInput`,
1575
+ appendInput: `${type}AppendInput`,
1576
+ incInput: `${type}IncInput`,
1577
+ onConflict: `${type}OnConflict`,
1578
+ pkColumnsInput: `${type}PkColumnsInput`,
1579
+ constraintEnum: `${type}Constraint`,
1580
+ updateColumnEnum: `${type}UpdateColumn`,
1581
+ mutationResponse: `${type}MutationResponse`,
1582
+ aggregate: `${type}Aggregate`,
1583
+ aggregateFields: `${type}AggregateFields`,
1584
+ sumFields: `${type}SumFields`,
1585
+ avgFields: `${type}AvgFields`,
1586
+ aggregateBoolExp: `${type}AggregateBoolExp`,
1587
+ aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
1588
+ aggregateGroupsRoot: `${root}AggregateGroups`,
1589
+ groupByColumnEnum: `${type}GroupByColumn`,
1590
+ groupByInput: `${type}GroupBy`,
1591
+ groupKey: `${type}GroupKey`,
1592
+ aggregateGroup: `${type}AggregateGroup`
1593
+ };
1594
+ }
1595
+ var CELL_TYPENAMES = {
1596
+ /** A plain table's cell: when and by whom it was last written. */
1597
+ stamp: "CellStamp",
1598
+ /** A resolved table's cell: which source won, when, by whom, and newer losers. */
1599
+ resolved: "ResolvedCell",
1600
+ /** A newer candidate that lost to the column's rule. */
1601
+ conflict: "CellConflict"
1602
+ };
1603
+ function cellsTypename(typename) {
1604
+ return `${typename}Cells`;
1605
+ }
1606
+
800
1607
  // src/predicate-v2/ast.ts
801
1608
  var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
802
1609
  function walkPolicy(node, visit2) {
@@ -833,7 +1640,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
833
1640
  var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
834
1641
  var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
835
1642
  var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
836
- function tokenize(source) {
1643
+ function tokenize2(source) {
837
1644
  const tokens = [];
838
1645
  let position = 0;
839
1646
  while (position < source.length) {
@@ -912,7 +1719,7 @@ function readString(source, start) {
912
1719
  }
913
1720
  throw new PolicyParseError("unterminated string literal", start);
914
1721
  }
915
- var Parser = class {
1722
+ var Parser2 = class {
916
1723
  constructor(tokens) {
917
1724
  this.tokens = tokens;
918
1725
  }
@@ -1086,7 +1893,7 @@ var Parser = class {
1086
1893
  function parsePolicy(source) {
1087
1894
  const trimmed = source.trim();
1088
1895
  if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
1089
- return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
1896
+ return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
1090
1897
  }
1091
1898
 
1092
1899
  // src/predicate-v2/compile.ts
@@ -1486,6 +2293,7 @@ var PolicyInjector = class {
1486
2293
  injectMutation(root) {
1487
2294
  const table = root.table;
1488
2295
  const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
2296
+ if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
1489
2297
  if (root.op === "insert" || root.op === "insertOne") {
1490
2298
  const insertPolicy = this.insert(table);
1491
2299
  if (predContainsExists(insertPolicy)) {
@@ -1527,6 +2335,24 @@ var PolicyInjector = class {
1527
2335
  }
1528
2336
  return { ...root, where, returning };
1529
2337
  }
2338
+ /**
2339
+ * Resolved writes are policed against the RESOLVED row, not the sparse
2340
+ * manual row: `where` gates the existing row (update/revert/delete) and
2341
+ * `insertCheck` is re-evaluated as a SELECT over the view after the write,
2342
+ * so link-traversing (exists) policies are representable here.
2343
+ */
2344
+ injectResolvedWrite(root, returning) {
2345
+ const table = root.table;
2346
+ if (root.resolvedAction === "insert") {
2347
+ const pins = (this.ctx.pins.get(table) ?? []).map(
2348
+ (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
2349
+ );
2350
+ return { ...root, pinnedColumns: pins, insertCheck: this.insert(table), returning };
2351
+ }
2352
+ const updatePolicy = this.update(table);
2353
+ const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
2354
+ return root.resolvedAction === "delete" ? { ...root, where, returning } : { ...root, where, insertCheck: updatePolicy, returning };
2355
+ }
1530
2356
  };
1531
2357
  function collectTableSet(roots, ctx) {
1532
2358
  const tables = /* @__PURE__ */ new Set();
@@ -1586,6 +2412,7 @@ function collectTableSet(roots, ctx) {
1586
2412
  break;
1587
2413
  case "mutation":
1588
2414
  if (root.cowSynced !== void 0) tables.add(root.cowSynced);
2415
+ for (const source of ctx.tables[root.table]?.resolved?.sources ?? []) tables.add(source.table);
1589
2416
  if (root.where) visitPred(root.where, root.table);
1590
2417
  if (root.insertCheck) visitPred(root.insertCheck, root.table);
1591
2418
  if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
@@ -1608,103 +2435,6 @@ function injectPolicies({ roots, ctx }) {
1608
2435
  return { roots: injected, tableSet: collectTableSet(injected, ctx) };
1609
2436
  }
1610
2437
 
1611
- // src/sdl/naming.ts
1612
- function camelCase(name) {
1613
- const segments = name.split("_").filter((segment) => segment.length > 0);
1614
- const first = segments[0];
1615
- if (first === void 0) return name;
1616
- const head = first.charAt(0).toLowerCase() + first.slice(1);
1617
- return head + segments.slice(1).map(capitalizeFirst).join("");
1618
- }
1619
- function pascalCase(name) {
1620
- return capitalizeFirst(camelCase(name));
1621
- }
1622
- function capitalizeFirst(segment) {
1623
- return segment.charAt(0).toUpperCase() + segment.slice(1);
1624
- }
1625
- function singularizeWord(word) {
1626
- if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
1627
- for (const suffix of ["sses", "xes", "ches", "shes"]) {
1628
- if (word.endsWith(suffix)) return word.slice(0, -2);
1629
- }
1630
- if (word.endsWith("ss")) return word;
1631
- if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
1632
- return word;
1633
- }
1634
- function typeName(table) {
1635
- const segments = table.split("_").filter((segment) => segment.length > 0);
1636
- const last = segments[segments.length - 1];
1637
- if (last === void 0) return pascalCase(table);
1638
- return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
1639
- }
1640
- function enumTypeName(name) {
1641
- return pascalCase(name);
1642
- }
1643
- var SCALAR_FOR_COLUMN_TYPE = {
1644
- uuid: "uuid",
1645
- text: "String",
1646
- integer: "Int",
1647
- bigint: "bigint",
1648
- numeric: "numeric",
1649
- double: "Float",
1650
- boolean: "Boolean",
1651
- timestamptz: "timestamptz",
1652
- date: "date",
1653
- jsonb: "jsonb"
1654
- };
1655
- function graphqlScalarFor(type) {
1656
- return SCALAR_FOR_COLUMN_TYPE[type];
1657
- }
1658
- var COLUMN_TYPE_BY_SCALAR = new Map([
1659
- ["ID", "text"],
1660
- ...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
1661
- ]);
1662
- function columnTypeForScalar(scalarName) {
1663
- return COLUMN_TYPE_BY_SCALAR.get(scalarName);
1664
- }
1665
- var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
1666
- var PLATFORM_USER_TYPENAME = "PlatformUser";
1667
- function tableNames(table) {
1668
- const root = camelCase(table);
1669
- const rootPascal = pascalCase(table);
1670
- const type = typeName(table);
1671
- return {
1672
- typename: type,
1673
- selectRoot: root,
1674
- byPkRoot: `${root}ByPk`,
1675
- aggregateRoot: `${root}Aggregate`,
1676
- insertRoot: `insert${rootPascal}`,
1677
- insertOneRoot: `insert${rootPascal}One`,
1678
- updateRoot: `update${rootPascal}`,
1679
- updateByPkRoot: `update${rootPascal}ByPk`,
1680
- deleteRoot: `delete${rootPascal}`,
1681
- deleteByPkRoot: `delete${rootPascal}ByPk`,
1682
- boolExp: `${type}BoolExp`,
1683
- orderBy: `${type}OrderBy`,
1684
- selectColumn: `${type}SelectColumn`,
1685
- insertInput: `${type}InsertInput`,
1686
- setInput: `${type}SetInput`,
1687
- appendInput: `${type}AppendInput`,
1688
- incInput: `${type}IncInput`,
1689
- onConflict: `${type}OnConflict`,
1690
- pkColumnsInput: `${type}PkColumnsInput`,
1691
- constraintEnum: `${type}Constraint`,
1692
- updateColumnEnum: `${type}UpdateColumn`,
1693
- mutationResponse: `${type}MutationResponse`,
1694
- aggregate: `${type}Aggregate`,
1695
- aggregateFields: `${type}AggregateFields`,
1696
- sumFields: `${type}SumFields`,
1697
- avgFields: `${type}AvgFields`,
1698
- aggregateBoolExp: `${type}AggregateBoolExp`,
1699
- aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
1700
- aggregateGroupsRoot: `${root}AggregateGroups`,
1701
- groupByColumnEnum: `${type}GroupByColumn`,
1702
- groupByInput: `${type}GroupBy`,
1703
- groupKey: `${type}GroupKey`,
1704
- aggregateGroup: `${type}AggregateGroup`
1705
- };
1706
- }
1707
-
1708
2438
  // src/compiler-gql/naming.ts
1709
2439
  var PLATFORM_USER_TABLE = "platformUsers";
1710
2440
  function typeNameForTable(table) {
@@ -1816,7 +2546,8 @@ function resolveColumnEntries(table, enums, diagnostics) {
1816
2546
  type,
1817
2547
  nullable: column.nullable,
1818
2548
  deprecated: column.deprecated,
1819
- onUpdateFn: column.onUpdateFn !== void 0
2549
+ onUpdateFn: column.onUpdateFn !== void 0,
2550
+ ...column.cell ? { cell: column.cell } : {}
1820
2551
  };
1821
2552
  });
1822
2553
  }
@@ -1853,8 +2584,8 @@ function buildCompileContext(rawCanonical) {
1853
2584
  const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
1854
2585
  const primaryKey = [];
1855
2586
  for (const ref of pkRefs) {
1856
- const resolved = logicalColumn(entries, ref);
1857
- if (!resolved) {
2587
+ const resolved2 = logicalColumn(entries, ref);
2588
+ if (!resolved2) {
1858
2589
  diagnostics.push({
1859
2590
  severity: "error",
1860
2591
  code: "unknown-pk-column",
@@ -1863,9 +2594,12 @@ function buildCompileContext(rawCanonical) {
1863
2594
  });
1864
2595
  continue;
1865
2596
  }
1866
- primaryKey.push(resolved);
2597
+ primaryKey.push(resolved2);
1867
2598
  }
1868
- const active = entries.filter((c) => !c.deprecated);
2599
+ const resolved = isView ? table.resolved : void 0;
2600
+ const data = entries.filter((c) => c.cell === void 0);
2601
+ const cells = cellMap(entries);
2602
+ const active = data.filter((c) => !c.deprecated);
1869
2603
  const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
1870
2604
  const constraints = [];
1871
2605
  if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
@@ -1890,12 +2624,14 @@ function buildCompileContext(rawCanonical) {
1890
2624
  columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
1891
2625
  primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
1892
2626
  links: [],
1893
- filterable: entries.map((c) => c.name),
1894
- orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
1895
- insertable: isView ? [] : active.map((c) => c.name),
1896
- settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
2627
+ filterable: data.map((c) => c.name),
2628
+ orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
2629
+ insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
2630
+ settable: resolved ? resolvedWritable(data).filter((name) => !primaryKey.includes(name)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
1897
2631
  appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
1898
2632
  ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
2633
+ ...cells ? { cells } : {},
2634
+ ...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey[0] }) } : {},
1899
2635
  constraints: isView ? [] : constraints,
1900
2636
  dynamicFilterMaxDepth: 6,
1901
2637
  dynamicFilterMaxNodes: 64
@@ -1968,7 +2704,7 @@ function buildCompileContext(rawCanonical) {
1968
2704
  compilePolicyFor("write", writePolicies);
1969
2705
  compilePolicyFor("insert", insertPolicies);
1970
2706
  compilePolicyFor("update", updatePolicies);
1971
- if (isView) {
2707
+ if (isView && table.resolved === void 0) {
1972
2708
  writePolicies.set(table.table, CONST_FALSE);
1973
2709
  insertPolicies.set(table.table, CONST_FALSE);
1974
2710
  updatePolicies.set(table.table, CONST_FALSE);
@@ -2007,6 +2743,7 @@ function buildCompileContext(rawCanonical) {
2007
2743
  const queryRoots = /* @__PURE__ */ new Map();
2008
2744
  const mutationRoots = /* @__PURE__ */ new Map();
2009
2745
  for (const table of relations) {
2746
+ if (table.internal === true) continue;
2010
2747
  const roots = queryRootNames(table.table);
2011
2748
  queryRoots.set(roots.select, { kind: "select", table: table.table });
2012
2749
  if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
@@ -2022,6 +2759,7 @@ function buildCompileContext(rawCanonical) {
2022
2759
  mutationRoots.set(field, { op, table: table.table });
2023
2760
  }
2024
2761
  }
2762
+ if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
2025
2763
  }
2026
2764
  for (const table of relations) {
2027
2765
  for (const mutation of table.cowMutations ?? []) {
@@ -2092,6 +2830,49 @@ function buildCompileContext(rawCanonical) {
2092
2830
  diagnostics
2093
2831
  };
2094
2832
  }
2833
+ function registerResolvedRoots({ table, mutationRoots }) {
2834
+ const names = tableNames(table.table);
2835
+ const canInsert = table.policies.write !== void 0 || table.policies.insert !== void 0;
2836
+ const canUpdate = table.policies.write !== void 0 || table.policies.update !== void 0;
2837
+ const add = (field, action) => mutationRoots.set(field, { op: "resolvedWrite", action, table: table.table });
2838
+ if (canInsert) add(names.insertOneRoot, "insert");
2839
+ if (canUpdate) {
2840
+ add(names.updateByPkRoot, "update");
2841
+ add(names.revertByPkRoot, "revert");
2842
+ if ((table.resolved?.deletedBy.length ?? 0) > 0) add(names.deleteByPkRoot, "delete");
2843
+ }
2844
+ }
2845
+ function cellMap(entries) {
2846
+ const cells = {};
2847
+ for (const entry of entries) {
2848
+ if (entry.cell) cells[entry.cell.of] = { ...cells[entry.cell.of], [entry.cell.part]: entry.name };
2849
+ }
2850
+ return Object.keys(cells).length > 0 ? cells : void 0;
2851
+ }
2852
+ function resolvedWritable(entries) {
2853
+ const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
2854
+ return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
2855
+ }
2856
+ function resolvedMeta({ resolved, pk }) {
2857
+ const manual = resolved.sources.find((source) => source.kind === "manual");
2858
+ return {
2859
+ pk,
2860
+ manual: manual?.name ?? "",
2861
+ deletedBy: [...resolved.deletedBy],
2862
+ ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
2863
+ sources: resolved.sources.map((source) => ({
2864
+ name: source.name,
2865
+ kind: source.kind,
2866
+ table: source.table,
2867
+ columns: [...source.columns],
2868
+ ...source.key !== void 0 ? { key: source.key } : {},
2869
+ ...source.fetch !== void 0 ? { fetch: source.fetch } : {},
2870
+ ...source.function !== void 0 ? { function: source.function } : {},
2871
+ ...source.prefetchDefaults !== void 0 ? { prefetchDefaults: source.prefetchDefaults } : {},
2872
+ ...source.writeback !== void 0 ? { writeback: source.writeback } : {}
2873
+ }))
2874
+ };
2875
+ }
2095
2876
  function flattenSelectionSet(selectionSet, fragments, stack) {
2096
2877
  const selections = [];
2097
2878
  for (const selection of selectionSet.selections) {
@@ -2149,6 +2930,136 @@ function computeOpsHash(opHashes) {
2149
2930
  const digest = crypto.createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
2150
2931
  return `sha256:${digest}`;
2151
2932
  }
2933
+ var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "localOnly"]);
2934
+ var fail = (message, code) => {
2935
+ throw new DocumentCompileError(message, code);
2936
+ };
2937
+ function readArgs(node, name) {
2938
+ return (node.directives ?? []).filter((directive) => directive.name.value === name).map((directive) => {
2939
+ const args = {};
2940
+ for (const arg of directive.arguments ?? []) {
2941
+ const key = arg.name.value;
2942
+ if (key === "source" || key === "maxStaleness") {
2943
+ if (arg.value.kind !== graphql.Kind.STRING) fail(`@${name}(${key}:) must be a string literal`, "invalid-source-directive");
2944
+ args[key] = arg.value.value;
2945
+ } else if (key === "mode" && name === "writeback") {
2946
+ if (arg.value.kind !== graphql.Kind.ENUM || !["SYNC", "ASYNC"].includes(arg.value.value)) {
2947
+ fail("@writeback(mode:) must be SYNC or ASYNC", "invalid-source-directive");
2948
+ }
2949
+ args.mode = arg.value.value === "SYNC" ? "sync" : "async";
2950
+ } else {
2951
+ fail(`@${name} does not take argument '${key}'`, "invalid-source-directive");
2952
+ }
2953
+ }
2954
+ if (!args.source) fail(`@${name} needs a source: argument`, "invalid-source-directive");
2955
+ return args;
2956
+ });
2957
+ }
2958
+ function prefetchSourceFor({ resolved, name, directive, table }) {
2959
+ if (!resolved) return fail(`@${directive} is only valid on resolved tables \u2014 '${table}' is not one`, "invalid-source-directive");
2960
+ const source = resolved.sources.find((entry) => entry.name === name);
2961
+ if (!source || source.kind !== "prefetch") {
2962
+ const options = resolved.sources.filter((entry) => entry.kind === "prefetch").map((entry) => entry.name);
2963
+ return fail(`@${directive}(source: "${name}") on '${table}' must name a prefetch source (${options.join(", ") || "none declared"})`, "invalid-source-directive");
2964
+ }
2965
+ return source;
2966
+ }
2967
+ function applyPrefetchDirectives({ node, root, meta }) {
2968
+ const directives = readArgs(node, "prefetch");
2969
+ for (const name of ["writeback", "localOnly"]) {
2970
+ if (readArgs(node, name).length > 0) fail(`@${name} is only valid on resolved-table mutation roots`, "invalid-source-directive");
2971
+ }
2972
+ if (directives.length === 0) return root;
2973
+ if (root.kind !== "select" && root.kind !== "byPk") {
2974
+ return fail(`@prefetch is only valid on select and byPk roots, not '${root.alias}'`, "invalid-source-directive");
2975
+ }
2976
+ const seen = /* @__PURE__ */ new Set();
2977
+ const plans = directives.map((directive) => {
2978
+ const source = prefetchSourceFor({ resolved: meta.resolved, name: directive.source, directive: "prefetch", table: root.table });
2979
+ if (seen.has(source.name)) fail(`@prefetch(source: "${source.name}") appears twice on '${root.alias}'`, "invalid-source-directive");
2980
+ seen.add(source.name);
2981
+ const maxStalenessMs = directive.maxStaleness === void 0 ? void 0 : parseDurationMs(directive.maxStaleness);
2982
+ if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
2983
+ fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
2984
+ }
2985
+ const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk: meta.resolved.pk, source, alias: root.alias });
2986
+ return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
2987
+ });
2988
+ return { ...root, prefetch: plans };
2989
+ }
2990
+ function selectKey({ where, pk, source, alias }) {
2991
+ const leaves = where.kind === "and" ? where.operands : [where];
2992
+ const pinned = leaves.find((leaf) => leaf.kind === "cmp" && leaf.column === pk && (leaf.op === "eq" || leaf.op === "in"));
2993
+ const shape = source.fetch?.shape ?? "single";
2994
+ if (!pinned || pinned.kind !== "cmp") {
2995
+ return fail(
2996
+ `@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)`,
2997
+ "prefetch-shape-mismatch"
2998
+ );
2999
+ }
3000
+ if (pinned.op === "in" && shape !== "batch") {
3001
+ 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");
3002
+ }
3003
+ return pinned.op === "eq" ? { one: pinned.value } : { many: pinned.value };
3004
+ }
3005
+ function applyWritebackDirectives({ node, root, meta }) {
3006
+ if (readArgs(node, "prefetch").length > 0) fail("@prefetch is only valid on query roots", "invalid-source-directive");
3007
+ const writebacks = readArgs(node, "writeback");
3008
+ const localOnly = readArgs(node, "localOnly");
3009
+ if (root.kind !== "mutation" || root.op !== "resolvedWrite" || !meta.resolved) {
3010
+ if (writebacks.length + localOnly.length > 0) fail("@writeback/@localOnly are only valid on resolved-table mutation roots", "invalid-source-directive");
3011
+ return root;
3012
+ }
3013
+ const resolved = meta.resolved;
3014
+ const declared = /* @__PURE__ */ new Map();
3015
+ const bindings = [];
3016
+ for (const directive of writebacks) {
3017
+ const source = prefetchSourceFor({ resolved, name: directive.source, directive: "writeback", table: root.table });
3018
+ if (!source.writeback) fail(`@writeback(source: "${source.name}"): source '${source.name}' declares no writeback columns`, "invalid-source-directive");
3019
+ if (root.resolvedAction !== "update") {
3020
+ fail(`@writeback on '${root.alias}': only update roots write back in v1 (use @localOnly for ${root.resolvedAction ?? "this"} roots)`, "writeback-shape-mismatch");
3021
+ }
3022
+ if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
3023
+ declared.set(source.name, "writeback");
3024
+ bindings.push({ source: source.name, mode: directive.mode ?? source.writeback.defaultMode });
3025
+ }
3026
+ for (const directive of localOnly) {
3027
+ const source = prefetchSourceFor({ resolved, name: directive.source, directive: "localOnly", table: root.table });
3028
+ if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
3029
+ declared.set(source.name, "localOnly");
3030
+ }
3031
+ assertWritebacksDeclared({ root, meta, resolved, declared });
3032
+ return {
3033
+ ...root,
3034
+ ...bindings.length > 0 ? { writeback: bindings } : {},
3035
+ ...localOnly.length > 0 ? { localOnly: localOnly.map((directive) => directive.source) } : {}
3036
+ };
3037
+ }
3038
+ function assertWritebacksDeclared({ root, meta, resolved, declared }) {
3039
+ const written = writtenColumns({ root, meta });
3040
+ for (const source of resolved.sources) {
3041
+ const columns = source.writeback?.columns.filter((column) => written.has(column)) ?? [];
3042
+ if (columns.length > 0 && !declared.has(source.name)) {
3043
+ fail(
3044
+ `'${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`,
3045
+ "writeback-undeclared"
3046
+ );
3047
+ }
3048
+ }
3049
+ }
3050
+ function writtenColumns({ root, meta }) {
3051
+ if (root.resolvedAction === "update") {
3052
+ if (!root.set || "slot" in root.set) return new Set(meta.settable);
3053
+ return new Set(root.set.entries.map((entry) => entry.column));
3054
+ }
3055
+ if (root.resolvedAction === "insert") {
3056
+ if (!root.objects || "slot" in root.objects) return new Set(meta.insertable);
3057
+ return new Set(root.objects.rows.flatMap((row) => row.map((entry) => entry.column)));
3058
+ }
3059
+ return /* @__PURE__ */ new Set();
3060
+ }
3061
+
3062
+ // src/compiler-gql/lower.ts
2152
3063
  var DEFAULT_LIMIT = 100;
2153
3064
  var MAX_LIMIT = 1e3;
2154
3065
  var MAX_GROUPS = 1e3;
@@ -2186,7 +3097,7 @@ var ORDER_DIRECTIONS = {
2186
3097
  DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
2187
3098
  };
2188
3099
  var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
2189
- function fail(message, code, candidates = []) {
3100
+ function fail2(message, code, candidates = []) {
2190
3101
  throw new DocumentCompileError(message, code, candidates);
2191
3102
  }
2192
3103
  function argsByName(field) {
@@ -2194,16 +3105,16 @@ function argsByName(field) {
2194
3105
  }
2195
3106
  function* aggregateBlockFields(selectionSet) {
2196
3107
  for (const node of selectionSet.selections) {
2197
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3108
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
2198
3109
  if (node.name.value === "__typename") continue;
2199
- if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
3110
+ if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
2200
3111
  yield node;
2201
3112
  }
2202
3113
  }
2203
3114
  function assertKnownArgs(field, allowed) {
2204
3115
  for (const arg of field.arguments ?? []) {
2205
3116
  if (!allowed.includes(arg.name.value)) {
2206
- fail(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
3117
+ fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
2207
3118
  }
2208
3119
  }
2209
3120
  }
@@ -2216,8 +3127,11 @@ var OperationLowerer = class {
2216
3127
  meta(table) {
2217
3128
  return this.ctx.tables[table];
2218
3129
  }
3130
+ /** A data column — cell-metadata columns are reachable only through `cells { … }`. */
2219
3131
  column(table, name) {
2220
- return this.meta(table).columns.find((c) => c.name === name);
3132
+ const meta = this.meta(table);
3133
+ const column = meta.columns.find((c) => c.name === name);
3134
+ return column && !isCellColumn(meta, name) ? column : void 0;
2221
3135
  }
2222
3136
  link(table, name) {
2223
3137
  return this.meta(table).links.find((l) => l.name === name);
@@ -2225,7 +3139,8 @@ var OperationLowerer = class {
2225
3139
  fieldCandidates(table) {
2226
3140
  const meta = this.meta(table);
2227
3141
  return [
2228
- ...meta.columns.map((c) => c.name),
3142
+ ...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
3143
+ ...meta.cells ? [CELLS_FIELD] : [],
2229
3144
  ...meta.links.map((l) => l.name),
2230
3145
  ...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
2231
3146
  ];
@@ -2234,7 +3149,7 @@ var OperationLowerer = class {
2234
3149
  slot(name, input, table) {
2235
3150
  const variable = this.variables.get(name);
2236
3151
  if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
2237
- fail(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
3152
+ fail2(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
2238
3153
  }
2239
3154
  return name;
2240
3155
  }
@@ -2259,14 +3174,14 @@ var OperationLowerer = class {
2259
3174
  const items = node.values.map((item) => {
2260
3175
  const lowered = this.lowerValue(item);
2261
3176
  if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
2262
- fail("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
3177
+ fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
2263
3178
  }
2264
3179
  return lowered.value;
2265
3180
  });
2266
3181
  return { kind: "literal", value: items };
2267
3182
  }
2268
3183
  case graphql.Kind.OBJECT:
2269
- fail("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
3184
+ fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
2270
3185
  }
2271
3186
  }
2272
3187
  // -------------------------------------------------------------------------
@@ -2276,7 +3191,7 @@ var OperationLowerer = class {
2276
3191
  if (node.kind === graphql.Kind.VARIABLE) {
2277
3192
  return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
2278
3193
  }
2279
- if (node.kind !== graphql.Kind.OBJECT) fail(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
3194
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
2280
3195
  const operands = [];
2281
3196
  for (const field of node.fields) {
2282
3197
  const key = field.name.value;
@@ -2307,45 +3222,45 @@ var OperationLowerer = class {
2307
3222
  continue;
2308
3223
  }
2309
3224
  }
2310
- fail(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
3225
+ fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
2311
3226
  }
2312
3227
  if (operands.length === 0) return CONST_TRUE;
2313
3228
  return operands.length === 1 ? operands[0] : { kind: "and", operands };
2314
3229
  }
2315
3230
  lowerComparison(node, column, table) {
2316
3231
  if (node.kind !== graphql.Kind.OBJECT) {
2317
- fail(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
3232
+ fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
2318
3233
  }
2319
3234
  return node.fields.map((field) => {
2320
3235
  const op = BOOLEXP_OPS[field.name.value];
2321
3236
  if (!op) {
2322
- fail(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
3237
+ fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
2323
3238
  }
2324
3239
  return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
2325
3240
  });
2326
3241
  }
2327
3242
  /** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
2328
3243
  lowerAggPredicate(node, link, target) {
2329
- if (node.kind !== graphql.Kind.OBJECT) fail(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
3244
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
2330
3245
  const countField = node.fields.find((f) => f.name.value === "count");
2331
3246
  if (!countField || node.fields.length !== 1) {
2332
- fail(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
3247
+ fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
2333
3248
  }
2334
- if (countField.value.kind !== graphql.Kind.OBJECT) fail(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
3249
+ if (countField.value.kind !== graphql.Kind.OBJECT) fail2(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
2335
3250
  let predicate;
2336
3251
  let where;
2337
3252
  for (const field of countField.value.fields) {
2338
3253
  if (field.name.value === "predicate") {
2339
3254
  if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
2340
- fail(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
3255
+ fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
2341
3256
  }
2342
3257
  const opField = field.value.fields[0];
2343
3258
  const opName = opField.name.value;
2344
3259
  if (!AGG_PREDICATE_OPS.includes(opName)) {
2345
- fail(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
3260
+ fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
2346
3261
  }
2347
3262
  if (opField.value.kind !== graphql.Kind.INT) {
2348
- fail(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
3263
+ fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
2349
3264
  }
2350
3265
  predicate = { op: opName.slice(1), value: Number(opField.value.value) };
2351
3266
  continue;
@@ -2354,9 +3269,9 @@ var OperationLowerer = class {
2354
3269
  where = this.lowerBoolExp(field.value, target);
2355
3270
  continue;
2356
3271
  }
2357
- fail(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
3272
+ fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
2358
3273
  }
2359
- if (!predicate) fail(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
3274
+ if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
2360
3275
  return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
2361
3276
  }
2362
3277
  lowerOrderBy(node, table) {
@@ -2366,7 +3281,7 @@ var OperationLowerer = class {
2366
3281
  const items = node.kind === graphql.Kind.LIST ? node.values : [node];
2367
3282
  const lowered = [];
2368
3283
  for (const item of items) {
2369
- if (item.kind !== graphql.Kind.OBJECT) fail(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
3284
+ if (item.kind !== graphql.Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
2370
3285
  for (const field of item.fields) {
2371
3286
  const key = field.name.value;
2372
3287
  const column = this.column(table, key);
@@ -2377,25 +3292,25 @@ var OperationLowerer = class {
2377
3292
  const link = this.link(table, key);
2378
3293
  if (link && link.kind !== "toMany") {
2379
3294
  if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
2380
- fail(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
3295
+ fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
2381
3296
  }
2382
3297
  const inner = field.value.fields[0];
2383
3298
  const targetColumn = this.column(link.target, inner.name.value);
2384
3299
  if (!targetColumn) {
2385
- fail(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
3300
+ fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
2386
3301
  }
2387
3302
  lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
2388
3303
  continue;
2389
3304
  }
2390
- fail(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
3305
+ fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
2391
3306
  }
2392
3307
  }
2393
3308
  return lowered;
2394
3309
  }
2395
3310
  lowerDirection(node, table, column) {
2396
- if (node.kind !== graphql.Kind.ENUM) fail(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
3311
+ if (node.kind !== graphql.Kind.ENUM) fail2(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
2397
3312
  const direction = ORDER_DIRECTIONS[node.value];
2398
- if (!direction) fail(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
3313
+ if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
2399
3314
  return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
2400
3315
  }
2401
3316
  lowerDistinctOn(node, table) {
@@ -2404,9 +3319,9 @@ var OperationLowerer = class {
2404
3319
  }
2405
3320
  const items = node.kind === graphql.Kind.LIST ? node.values : [node];
2406
3321
  return items.map((item) => {
2407
- if (item.kind !== graphql.Kind.ENUM) fail(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
3322
+ if (item.kind !== graphql.Kind.ENUM) fail2(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
2408
3323
  if (!this.column(table, item.value)) {
2409
- fail(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3324
+ fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
2410
3325
  }
2411
3326
  return item.value;
2412
3327
  });
@@ -2417,16 +3332,16 @@ var OperationLowerer = class {
2417
3332
  if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
2418
3333
  if (node.kind === graphql.Kind.INT) {
2419
3334
  const value = Number(node.value);
2420
- if (value < 1) fail("limit must be a positive integer", "invalid-limit");
3335
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
2421
3336
  return { ...base, value: Math.min(value, MAX_LIMIT) };
2422
3337
  }
2423
- fail("limit must be an integer literal or a variable", "invalid-limit");
3338
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
2424
3339
  }
2425
3340
  lowerOffset(node) {
2426
3341
  if (!node) return void 0;
2427
3342
  if (node.kind === graphql.Kind.VARIABLE) return { variable: node.name.value };
2428
3343
  if (node.kind === graphql.Kind.INT) return { value: Number(node.value) };
2429
- fail("offset must be an integer literal or a variable", "invalid-offset");
3344
+ fail2("offset must be an integer literal or a variable", "invalid-offset");
2430
3345
  }
2431
3346
  // -------------------------------------------------------------------------
2432
3347
  // Selections
@@ -2435,9 +3350,9 @@ var OperationLowerer = class {
2435
3350
  const meta = this.meta(table);
2436
3351
  const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
2437
3352
  for (const node of selectionSet.selections) {
2438
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3353
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
2439
3354
  if (node.directives && node.directives.length > 0) {
2440
- fail(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
3355
+ fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
2441
3356
  }
2442
3357
  const name = node.name.value;
2443
3358
  if (name === "__typename") continue;
@@ -2445,14 +3360,19 @@ var OperationLowerer = class {
2445
3360
  const column = this.column(table, name);
2446
3361
  if (column) {
2447
3362
  if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
2448
- fail(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
3363
+ fail2(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
2449
3364
  }
2450
3365
  selection.columns.push({ alias, column: name });
2451
3366
  continue;
2452
3367
  }
3368
+ if (name === CELLS_FIELD && meta.cells) {
3369
+ if (selection.cells) fail2(`'${CELLS_FIELD}' may be selected once per object`, "invalid-selection");
3370
+ selection.cells = this.lowerCells(node, table, alias);
3371
+ continue;
3372
+ }
2453
3373
  const link = this.link(table, name);
2454
3374
  if (link) {
2455
- if (!node.selectionSet) fail(`link '${table}.${name}' needs a selection set`, "invalid-selection");
3375
+ if (!node.selectionSet) fail2(`link '${table}.${name}' needs a selection set`, "invalid-selection");
2456
3376
  const relation = {
2457
3377
  alias,
2458
3378
  link: name,
@@ -2471,7 +3391,7 @@ var OperationLowerer = class {
2471
3391
  if (offset) relation.offset = offset;
2472
3392
  }
2473
3393
  } else if (node.arguments && node.arguments.length > 0) {
2474
- fail(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
3394
+ fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
2475
3395
  }
2476
3396
  selection.relations.push(relation);
2477
3397
  continue;
@@ -2479,7 +3399,7 @@ var OperationLowerer = class {
2479
3399
  if (name.endsWith("Aggregate")) {
2480
3400
  const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
2481
3401
  if (aggLink && aggLink.kind === "toMany") {
2482
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3402
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2483
3403
  assertKnownArgs(node, ["where"]);
2484
3404
  const whereArg = argsByName(node).get("where");
2485
3405
  const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
@@ -2492,10 +3412,57 @@ var OperationLowerer = class {
2492
3412
  continue;
2493
3413
  }
2494
3414
  }
2495
- fail(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
3415
+ fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
2496
3416
  }
2497
3417
  return selection;
2498
3418
  }
3419
+ /**
3420
+ * `cells { <column> { at by … } }` — typed per-cell metadata. Plain tables
3421
+ * offer `at`/`by`; resolved tables add the winning `source` and newer
3422
+ * `conflicts { source at value }`.
3423
+ */
3424
+ lowerCells(node, table, alias) {
3425
+ const meta = this.meta(table);
3426
+ const cells = meta.cells ?? {};
3427
+ if (node.arguments && node.arguments.length > 0) fail2(`'${CELLS_FIELD}' takes no arguments`, "invalid-selection");
3428
+ if (!node.selectionSet) fail2(`'${CELLS_FIELD}' needs a selection set`, "invalid-selection");
3429
+ const fields = [];
3430
+ for (const cellNode of fieldNodes(node.selectionSet)) {
3431
+ const column = cellNode.name.value;
3432
+ const parts = cells[column];
3433
+ if (!parts || parts.at === void 0 && parts.source === void 0) {
3434
+ fail2(`unknown cell '${column}' on '${table}'`, "unknown-field", Object.keys(cells));
3435
+ }
3436
+ if (!cellNode.selectionSet) fail2(`cell '${column}' needs a selection set`, "invalid-selection");
3437
+ const resolved = parts.source !== void 0;
3438
+ const allowed = resolved ? ["source", "at", "by", "conflicts"] : ["at", "by"];
3439
+ const selected = [];
3440
+ for (const partNode of fieldNodes(cellNode.selectionSet)) {
3441
+ const part = partNode.name.value;
3442
+ if (!allowed.includes(part)) fail2(`unknown field '${part}' on cell '${column}'`, "unknown-field", allowed);
3443
+ const partAlias = partNode.alias?.value ?? part;
3444
+ if (part !== "conflicts") {
3445
+ if (partNode.selectionSet) fail2(`cell field '${part}' takes no sub-selection`, "invalid-selection");
3446
+ selected.push({ alias: partAlias, part });
3447
+ continue;
3448
+ }
3449
+ if (!partNode.selectionSet) fail2(`'conflicts' needs a selection set`, "invalid-selection");
3450
+ const conflictFields = fieldNodes(partNode.selectionSet).map((fieldNode) => {
3451
+ const field = fieldNode.name.value;
3452
+ if (field !== "source" && field !== "at" && field !== "value") fail2(`unknown field '${field}' on a cell conflict`, "unknown-field", ["source", "at", "value"]);
3453
+ return { alias: fieldNode.alias?.value ?? field, field };
3454
+ });
3455
+ selected.push({ alias: partAlias, part: "conflicts", fields: conflictFields, typename: CELL_TYPENAMES.conflict });
3456
+ }
3457
+ fields.push({
3458
+ alias: cellNode.alias?.value ?? column,
3459
+ column,
3460
+ typename: resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp,
3461
+ parts: selected
3462
+ });
3463
+ }
3464
+ return { alias, typename: cellsTypename(meta.typename), fields };
3465
+ }
2499
3466
  /** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
2500
3467
  lowerAggregateSelection(selectionSet, table) {
2501
3468
  const result = {};
@@ -2503,17 +3470,17 @@ var OperationLowerer = class {
2503
3470
  for (const node of aggregateBlockFields(selectionSet)) {
2504
3471
  const name = node.name.value;
2505
3472
  if (name === "nodes") {
2506
- if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
3473
+ if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
2507
3474
  result.nodes = this.lowerSelection(node.selectionSet, table);
2508
3475
  continue;
2509
3476
  }
2510
- if (name !== "aggregate") fail(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
2511
- if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3477
+ if (name !== "aggregate") fail2(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
3478
+ if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
2512
3479
  sawAggregate = true;
2513
- if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
3480
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
2514
3481
  Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
2515
3482
  }
2516
- if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
3483
+ if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
2517
3484
  return result;
2518
3485
  }
2519
3486
  /**
@@ -2530,24 +3497,24 @@ var OperationLowerer = class {
2530
3497
  for (const node of aggregateBlockFields(selectionSet)) {
2531
3498
  const name = node.name.value;
2532
3499
  if (name === "key") {
2533
- if (sawKey) fail("one key block per grouped aggregate", "invalid-aggregate");
3500
+ if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
2534
3501
  sawKey = true;
2535
- if (!node.selectionSet) fail("'key' needs a selection set", "invalid-aggregate");
3502
+ if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
2536
3503
  result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
2537
3504
  continue;
2538
3505
  }
2539
3506
  if (name === "nodes") {
2540
- fail("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
3507
+ fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
2541
3508
  }
2542
- if (name !== "aggregate") fail(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
2543
- if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3509
+ if (name !== "aggregate") fail2(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
3510
+ if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
2544
3511
  sawAggregate = true;
2545
- if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
3512
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
2546
3513
  Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
2547
3514
  }
2548
3515
  const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
2549
3516
  if (result.key.length === 0 && !rendersAggregate) {
2550
- fail(
3517
+ fail2(
2551
3518
  "grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
2552
3519
  "invalid-aggregate",
2553
3520
  ["key", "aggregate"]
@@ -2564,14 +3531,14 @@ var OperationLowerer = class {
2564
3531
  lowerGroupKeySelection(selectionSet, table, groupColumns) {
2565
3532
  const key = [];
2566
3533
  for (const node of selectionSet.selections) {
2567
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3534
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
2568
3535
  const name = node.name.value;
2569
3536
  if (name === "__typename") continue;
2570
3537
  if (!this.column(table, name)) {
2571
- fail(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
3538
+ fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
2572
3539
  }
2573
3540
  if (!groupColumns.has(name)) {
2574
- fail(
3541
+ fail2(
2575
3542
  `key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
2576
3543
  "invalid-group-key",
2577
3544
  [...groupColumns]
@@ -2594,21 +3561,21 @@ var OperationLowerer = class {
2594
3561
  if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
2595
3562
  const distinct = args.get("distinct");
2596
3563
  if (distinct) {
2597
- if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
3564
+ if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
2598
3565
  count.distinct = distinct.value.value;
2599
3566
  }
2600
3567
  result.count = count;
2601
3568
  continue;
2602
3569
  }
2603
3570
  if (aggName === "sum" || aggName === "avg") {
2604
- if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
3571
+ if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
2605
3572
  const columns = [];
2606
3573
  for (const columnField of aggField.selectionSet.selections) {
2607
- if (columnField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3574
+ if (columnField.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
2608
3575
  if (columnField.name.value === "__typename") continue;
2609
3576
  const column = this.column(table, columnField.name.value);
2610
3577
  if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
2611
- fail(
3578
+ fail2(
2612
3579
  `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
2613
3580
  "invalid-aggregate",
2614
3581
  this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
@@ -2619,7 +3586,7 @@ var OperationLowerer = class {
2619
3586
  result[aggName] = columns;
2620
3587
  continue;
2621
3588
  }
2622
- fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
3589
+ fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
2623
3590
  }
2624
3591
  return result;
2625
3592
  }
@@ -2629,41 +3596,43 @@ var OperationLowerer = class {
2629
3596
  lowerRoots(operation, kind) {
2630
3597
  const roots = [];
2631
3598
  for (const node of operation.selectionSet.selections) {
2632
- if (node.kind !== graphql.Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
3599
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
2633
3600
  if (node.selectionSet) graphql.visit(node.selectionSet, {
2634
3601
  Directive(directive) {
2635
3602
  if (directive.name.value === "assertAffectedRows") {
2636
- fail("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
3603
+ fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
2637
3604
  }
2638
3605
  }
2639
3606
  });
2640
3607
  const assertion = this.parseAffectedRowAssertion(node, kind);
2641
3608
  const gate = this.parseRootDirectives(node);
2642
3609
  if (gate.omit) continue;
2643
- const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
3610
+ const lowered = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
3611
+ const meta = this.meta(lowered.table);
3612
+ const root = kind === "query" ? applyPrefetchDirectives({ node, root: lowered, meta }) : applyWritebackDirectives({ node, root: lowered, meta });
2644
3613
  const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
2645
3614
  roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
2646
3615
  }
2647
- if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
3616
+ if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
2648
3617
  return roots;
2649
3618
  }
2650
3619
  parseAffectedRowAssertion(node, kind) {
2651
3620
  const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
2652
3621
  if (!assertions.length) return void 0;
2653
3622
  if (kind !== "mutation" || assertions.length !== 1) {
2654
- fail("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
3623
+ fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
2655
3624
  }
2656
3625
  if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
2657
- fail("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
3626
+ fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
2658
3627
  }
2659
3628
  const args = assertions[0].arguments;
2660
3629
  const arg = args?.[0];
2661
3630
  if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
2662
- fail("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
3631
+ fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
2663
3632
  }
2664
3633
  const count = Number(arg.value.value);
2665
3634
  if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
2666
- fail("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
3635
+ fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
2667
3636
  }
2668
3637
  return count;
2669
3638
  }
@@ -2673,18 +3642,19 @@ var OperationLowerer = class {
2673
3642
  for (const directive of node.directives ?? []) {
2674
3643
  const name = directive.name.value;
2675
3644
  if (name === "assertAffectedRows") continue;
3645
+ if (SOURCE_DIRECTIVES.has(name)) continue;
2676
3646
  if (name !== "include" && name !== "skip") {
2677
- fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
3647
+ fail2(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
2678
3648
  }
2679
3649
  const ifArg = directive.arguments?.find((a) => a.name.value === "if");
2680
- if (!ifArg) fail(`@${name} needs an if: argument`, "invalid-directive");
3650
+ if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
2681
3651
  if (ifArg.value.kind === graphql.Kind.BOOLEAN) {
2682
3652
  const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
2683
3653
  if (!keep) omit = true;
2684
3654
  continue;
2685
3655
  }
2686
- if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
2687
- if (includeIf) fail("at most one of @include/@skip per root field", "invalid-directive");
3656
+ if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail2(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
3657
+ if (includeIf) fail2("at most one of @include/@skip per root field", "invalid-directive");
2688
3658
  includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
2689
3659
  }
2690
3660
  return omit ? { omit: true } : { includeIf, omit: false };
@@ -2693,12 +3663,12 @@ var OperationLowerer = class {
2693
3663
  const name = node.name.value;
2694
3664
  const alias = node.alias?.value ?? name;
2695
3665
  const rootInfo = this.ctx.queryRoots.get(name);
2696
- if (!rootInfo) fail(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
3666
+ if (!rootInfo) fail2(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
2697
3667
  const { table } = rootInfo;
2698
3668
  const args = argsByName(node);
2699
3669
  if (rootInfo.kind === "select") {
2700
3670
  assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
2701
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3671
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2702
3672
  const where = args.get("where");
2703
3673
  const orderBy = args.get("orderBy");
2704
3674
  const distinctOn = args.get("distinctOn");
@@ -2717,7 +3687,7 @@ var OperationLowerer = class {
2717
3687
  };
2718
3688
  }
2719
3689
  if (rootInfo.kind === "byPk") {
2720
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3690
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2721
3691
  const pk = this.lowerPkArgs(node, table);
2722
3692
  return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
2723
3693
  }
@@ -2725,7 +3695,7 @@ var OperationLowerer = class {
2725
3695
  return this.lowerAggregateGroupsRoot(node, { alias, table });
2726
3696
  }
2727
3697
  assertKnownArgs(node, ["where"]);
2728
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3698
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2729
3699
  const whereArg = args.get("where");
2730
3700
  const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
2731
3701
  return {
@@ -2739,10 +3709,10 @@ var OperationLowerer = class {
2739
3709
  /** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
2740
3710
  lowerAggregateGroupsRoot(node, { alias, table }) {
2741
3711
  assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
2742
- if (!node.selectionSet) fail(`'${node.name.value}' needs a selection set`, "invalid-selection");
3712
+ if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
2743
3713
  const args = argsByName(node);
2744
3714
  const groupByArg = args.get("groupBy");
2745
- if (!groupByArg) fail(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
3715
+ if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
2746
3716
  const whereArg = args.get("where");
2747
3717
  const groupBy = this.lowerGroupBy(groupByArg.value, table);
2748
3718
  const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
@@ -2772,55 +3742,55 @@ var OperationLowerer = class {
2772
3742
  */
2773
3743
  lowerGroupBy(node, table) {
2774
3744
  if (node.kind === graphql.Kind.VARIABLE) {
2775
- fail(
3745
+ fail2(
2776
3746
  `groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
2777
3747
  "dynamic-group-by"
2778
3748
  );
2779
3749
  }
2780
3750
  const items = node.kind === graphql.Kind.LIST ? node.values : [node];
2781
- if (items.length === 0) fail(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
3751
+ if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
2782
3752
  const seen = /* @__PURE__ */ new Set();
2783
3753
  return items.map((item) => {
2784
3754
  if (item.kind !== graphql.Kind.OBJECT) {
2785
- fail(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
3755
+ fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
2786
3756
  }
2787
3757
  const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
2788
3758
  for (const key of fields.keys()) {
2789
3759
  if (key !== "column" && key !== "bucket") {
2790
- fail(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
3760
+ fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
2791
3761
  }
2792
3762
  }
2793
3763
  const columnNode = fields.get("column");
2794
- if (!columnNode) fail(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
3764
+ if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
2795
3765
  if (columnNode.kind !== graphql.Kind.ENUM) {
2796
- fail(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
3766
+ fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
2797
3767
  }
2798
3768
  const column = this.column(table, columnNode.value);
2799
3769
  if (!column) {
2800
- fail(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3770
+ fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
2801
3771
  }
2802
3772
  if (column.type === "jsonb") {
2803
- fail(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
3773
+ fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
2804
3774
  }
2805
3775
  const bucketNode = fields.get("bucket");
2806
3776
  let bucket;
2807
3777
  if (bucketNode) {
2808
3778
  if (bucketNode.kind !== graphql.Kind.ENUM) {
2809
- fail(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3779
+ fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
2810
3780
  }
2811
3781
  bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
2812
3782
  if (!bucket) {
2813
- fail(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3783
+ fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
2814
3784
  }
2815
3785
  if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
2816
- fail(
3786
+ fail2(
2817
3787
  `groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
2818
3788
  "invalid-group-by"
2819
3789
  );
2820
3790
  }
2821
3791
  }
2822
3792
  if (seen.has(columnNode.value)) {
2823
- fail(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
3793
+ fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
2824
3794
  }
2825
3795
  seen.add(columnNode.value);
2826
3796
  return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
@@ -2833,10 +3803,10 @@ var OperationLowerer = class {
2833
3803
  if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
2834
3804
  if (node.kind === graphql.Kind.INT) {
2835
3805
  const value = Number(node.value);
2836
- if (value < 1) fail("limit must be a positive integer", "invalid-limit");
3806
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
2837
3807
  return { ...base, value: Math.min(value, MAX_GROUPS) };
2838
3808
  }
2839
- fail("limit must be an integer literal or a variable", "invalid-limit");
3809
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
2840
3810
  }
2841
3811
  /** byPk-style pk arguments: one argument per primary-key column, exactly. */
2842
3812
  lowerPkArgs(node, table) {
@@ -2845,7 +3815,7 @@ var OperationLowerer = class {
2845
3815
  assertKnownArgs(node, meta.primaryKey);
2846
3816
  return meta.primaryKey.map((column) => {
2847
3817
  const arg = args.get(column);
2848
- if (!arg) fail(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
3818
+ if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
2849
3819
  return { column, value: this.lowerValue(arg.value) };
2850
3820
  });
2851
3821
  }
@@ -2856,19 +3826,22 @@ var OperationLowerer = class {
2856
3826
  const name = node.name.value;
2857
3827
  const alias = node.alias?.value ?? name;
2858
3828
  const rootInfo = this.ctx.mutationRoots.get(name);
2859
- if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
3829
+ if (!rootInfo) fail2(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
2860
3830
  if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
2861
3831
  return this.lowerCowRoot(node, rootInfo);
2862
3832
  }
3833
+ if (rootInfo.op === "resolvedWrite") {
3834
+ return this.lowerResolvedRoot(node, rootInfo);
3835
+ }
2863
3836
  const { op, table } = rootInfo;
2864
3837
  const args = argsByName(node);
2865
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3838
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2866
3839
  const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
2867
3840
  switch (op) {
2868
3841
  case "insert": {
2869
3842
  assertKnownArgs(node, ["objects", "onConflict"]);
2870
3843
  const objects = args.get("objects");
2871
- if (!objects) fail(`'${name}' needs objects:`, "missing-argument");
3844
+ if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
2872
3845
  const response = this.lowerMutationResponse(node.selectionSet, table);
2873
3846
  return {
2874
3847
  ...base,
@@ -2880,7 +3853,7 @@ var OperationLowerer = class {
2880
3853
  case "insertOne": {
2881
3854
  assertKnownArgs(node, ["object", "onConflict"]);
2882
3855
  const object = args.get("object");
2883
- if (!object) fail(`'${name}' needs object:`, "missing-argument");
3856
+ if (!object) fail2(`'${name}' needs object:`, "missing-argument");
2884
3857
  return {
2885
3858
  ...base,
2886
3859
  objects: this.lowerInsertObjects(object.value, table, { list: false }),
@@ -2891,7 +3864,7 @@ var OperationLowerer = class {
2891
3864
  case "update": {
2892
3865
  assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
2893
3866
  const where = args.get("where");
2894
- if (!where) fail(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
3867
+ if (!where) fail2(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
2895
3868
  return {
2896
3869
  ...base,
2897
3870
  ...this.lowerMutationResponse(node.selectionSet, table),
@@ -2902,7 +3875,7 @@ var OperationLowerer = class {
2902
3875
  case "updateByPk": {
2903
3876
  assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
2904
3877
  const pkColumns = args.get("pkColumns");
2905
- if (!pkColumns) fail(`'${name}' needs pkColumns:`, "missing-argument");
3878
+ if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
2906
3879
  return {
2907
3880
  ...base,
2908
3881
  pk: this.lowerPkColumnsArg(pkColumns.value, table),
@@ -2913,7 +3886,7 @@ var OperationLowerer = class {
2913
3886
  case "delete": {
2914
3887
  assertKnownArgs(node, ["where"]);
2915
3888
  const where = args.get("where");
2916
- if (!where) fail(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
3889
+ if (!where) fail2(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
2917
3890
  return {
2918
3891
  ...base,
2919
3892
  ...this.lowerMutationResponse(node.selectionSet, table),
@@ -2940,11 +3913,11 @@ var OperationLowerer = class {
2940
3913
  const name = node.name.value;
2941
3914
  const alias = node.alias?.value ?? name;
2942
3915
  const { table } = rootInfo;
2943
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3916
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2944
3917
  const meta = this.meta(table);
2945
3918
  const args = argsByName(node);
2946
3919
  const pkArg = args.get(rootInfo.pk);
2947
- if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
3920
+ if (!pkArg) fail2(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
2948
3921
  const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
2949
3922
  const base = {
2950
3923
  kind: "mutation",
@@ -2965,24 +3938,78 @@ var OperationLowerer = class {
2965
3938
  const entries = rootInfo.set.map((column) => {
2966
3939
  const arg = args.get(column);
2967
3940
  if (!arg) {
2968
- fail(
3941
+ fail2(
2969
3942
  `'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
2970
3943
  "missing-argument",
2971
3944
  [...rootInfo.set]
2972
3945
  );
2973
3946
  }
2974
3947
  if (!meta.settable.includes(column)) {
2975
- fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
3948
+ fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
2976
3949
  }
2977
3950
  return { column, value: this.lowerValue(arg.value) };
2978
3951
  });
2979
3952
  return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
2980
3953
  }
3954
+ /**
3955
+ * Resolved-table roots: insertOne(object), updateByPk(pkColumns, _set),
3956
+ * deleteByPk(pk…), revertByPk(pkColumns, columns). The executor writes only
3957
+ * the touched cells into the manual source; `returning` reads the view.
3958
+ */
3959
+ lowerResolvedRoot(node, rootInfo) {
3960
+ const name = node.name.value;
3961
+ const alias = node.alias?.value ?? name;
3962
+ const { table, action } = rootInfo;
3963
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3964
+ const args = argsByName(node);
3965
+ const base = {
3966
+ kind: "mutation",
3967
+ op: "resolvedWrite",
3968
+ resolvedAction: action,
3969
+ alias,
3970
+ table,
3971
+ pinnedColumns: [],
3972
+ affectedRows: false,
3973
+ returning: this.lowerSelection(node.selectionSet, table)
3974
+ };
3975
+ switch (action) {
3976
+ case "insert": {
3977
+ assertKnownArgs(node, ["object"]);
3978
+ const object = args.get("object");
3979
+ if (!object) fail2(`'${name}' needs object:`, "missing-argument");
3980
+ return { ...base, objects: this.lowerInsertObjects(object.value, table, { list: false }) };
3981
+ }
3982
+ case "update": {
3983
+ assertKnownArgs(node, ["pkColumns", "_set"]);
3984
+ const pkColumns = args.get("pkColumns");
3985
+ if (!pkColumns || !args.get("_set")) fail2(`'${name}' needs pkColumns: and _set:`, "missing-argument");
3986
+ return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), ...this.lowerWriteValues(args, table) };
3987
+ }
3988
+ case "delete":
3989
+ return { ...base, pk: this.lowerPkArgs(node, table) };
3990
+ case "revert": {
3991
+ assertKnownArgs(node, ["pkColumns", "columns"]);
3992
+ const pkColumns = args.get("pkColumns");
3993
+ const columns = args.get("columns");
3994
+ if (!pkColumns || !columns) fail2(`'${name}' needs pkColumns: and columns:`, "missing-argument");
3995
+ const items = columns.value.kind === graphql.Kind.LIST ? columns.value.values : [columns.value];
3996
+ const meta = this.meta(table);
3997
+ const revertColumns = items.map((item) => {
3998
+ if (item.kind !== graphql.Kind.ENUM || !meta.settable.includes(item.value)) {
3999
+ fail2(`'${name}' columns: must list update-column enum values written in the document`, "invalid-revert-columns", meta.settable);
4000
+ }
4001
+ return item.value;
4002
+ });
4003
+ if (revertColumns.length === 0) fail2(`'${name}' columns: must name at least one column`, "invalid-revert-columns");
4004
+ return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), revertColumns: [...new Set(revertColumns)] };
4005
+ }
4006
+ }
4007
+ }
2981
4008
  lowerMutationResponse(selectionSet, table) {
2982
4009
  let affectedRows = false;
2983
4010
  let returning = null;
2984
4011
  for (const node of selectionSet.selections) {
2985
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
4012
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
2986
4013
  const name = node.name.value;
2987
4014
  if (name === "__typename") continue;
2988
4015
  if (name === "affectedRows") {
@@ -2990,34 +4017,34 @@ var OperationLowerer = class {
2990
4017
  continue;
2991
4018
  }
2992
4019
  if (name === "returning") {
2993
- if (!node.selectionSet) fail("'returning' needs a selection set", "invalid-selection");
4020
+ if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
2994
4021
  returning = this.lowerSelection(node.selectionSet, table);
2995
4022
  continue;
2996
4023
  }
2997
- fail(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
4024
+ fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
2998
4025
  }
2999
4026
  return { affectedRows, returning };
3000
4027
  }
3001
4028
  lowerInsertObjects(node, table, { list }) {
3002
4029
  if (node.kind === graphql.Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
3003
4030
  if (list) {
3004
- if (node.kind !== graphql.Kind.LIST) fail(`objects: must be a list literal or a variable`, "invalid-insert");
4031
+ if (node.kind !== graphql.Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
3005
4032
  return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
3006
4033
  }
3007
4034
  return { rows: [this.lowerInsertRow(node, table)] };
3008
4035
  }
3009
4036
  lowerInsertRow(node, table) {
3010
- if (node.kind !== graphql.Kind.OBJECT) fail(`insert rows on '${table}' must be objects`, "invalid-insert");
4037
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
3011
4038
  const meta = this.meta(table);
3012
4039
  return node.fields.map((field) => {
3013
4040
  const key = field.name.value;
3014
4041
  if (this.link(table, key)) {
3015
- fail(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
4042
+ fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
3016
4043
  }
3017
4044
  if (!meta.insertable.includes(key)) {
3018
4045
  const column = this.column(table, key);
3019
- if (column) fail(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
3020
- fail(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
4046
+ if (column) fail2(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
4047
+ fail2(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
3021
4048
  }
3022
4049
  return { column: key, value: this.lowerValue(field.value) };
3023
4050
  });
@@ -3026,32 +4053,32 @@ var OperationLowerer = class {
3026
4053
  if (!arg) return {};
3027
4054
  const node = arg.value;
3028
4055
  if (node.kind === graphql.Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
3029
- if (node.kind !== graphql.Kind.OBJECT) fail("onConflict: must be an object literal or a variable", "invalid-on-conflict");
4056
+ if (node.kind !== graphql.Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
3030
4057
  const meta = this.meta(table);
3031
4058
  let constraint;
3032
4059
  const updateColumns = [];
3033
4060
  for (const field of node.fields) {
3034
4061
  if (field.name.value === "constraint") {
3035
- if (field.value.kind !== graphql.Kind.ENUM) fail("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
4062
+ if (field.value.kind !== graphql.Kind.ENUM) fail2("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
3036
4063
  constraint = field.value.value;
3037
4064
  continue;
3038
4065
  }
3039
4066
  if (field.name.value === "updateColumns") {
3040
4067
  const items = field.value.kind === graphql.Kind.LIST ? field.value.values : [field.value];
3041
4068
  for (const item of items) {
3042
- if (item.kind !== graphql.Kind.ENUM) fail("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
4069
+ if (item.kind !== graphql.Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
3043
4070
  if (!meta.settable.includes(item.value)) {
3044
- fail(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
4071
+ fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
3045
4072
  }
3046
4073
  updateColumns.push(item.value);
3047
4074
  }
3048
4075
  continue;
3049
4076
  }
3050
- fail(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
4077
+ fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
3051
4078
  }
3052
- if (!constraint) fail("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
4079
+ if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
3053
4080
  if (!meta.constraints.some((c) => c.name === constraint)) {
3054
- fail(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
4081
+ fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
3055
4082
  }
3056
4083
  return { onConflict: { constraint, updateColumns } };
3057
4084
  }
@@ -3069,36 +4096,36 @@ var OperationLowerer = class {
3069
4096
  const inc = args.get("_inc");
3070
4097
  if (inc) {
3071
4098
  if (inc.value.kind === graphql.Kind.VARIABLE) {
3072
- fail("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
4099
+ fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
3073
4100
  }
3074
4101
  const numeric = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
3075
4102
  result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric, "_inc") };
3076
4103
  }
3077
4104
  if (!result.set && !result.append && !result.inc) {
3078
- fail(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
4105
+ fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
3079
4106
  }
3080
4107
  return result;
3081
4108
  }
3082
4109
  lowerColumnEntries(node, table, whitelist, argName) {
3083
- if (node.kind !== graphql.Kind.OBJECT) fail(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
4110
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
3084
4111
  return node.fields.map((field) => {
3085
4112
  const key = field.name.value;
3086
4113
  if (!whitelist.includes(key)) {
3087
- fail(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
4114
+ fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
3088
4115
  }
3089
4116
  return { column: key, value: this.lowerValue(field.value) };
3090
4117
  });
3091
4118
  }
3092
4119
  lowerPkColumnsArg(node, table) {
3093
- if (node.kind !== graphql.Kind.OBJECT) fail(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
4120
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
3094
4121
  const meta = this.meta(table);
3095
4122
  const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
3096
4123
  for (const key of provided.keys()) {
3097
- if (!meta.primaryKey.includes(key)) fail(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
4124
+ if (!meta.primaryKey.includes(key)) fail2(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
3098
4125
  }
3099
4126
  return meta.primaryKey.map((column) => {
3100
4127
  const value = provided.get(column);
3101
- if (!value) fail(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
4128
+ if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
3102
4129
  return { column, value: this.lowerValue(value) };
3103
4130
  });
3104
4131
  }
@@ -3107,6 +4134,19 @@ function lowerOperation({ restricted, ctx }) {
3107
4134
  const lowerer = new OperationLowerer(ctx, restricted);
3108
4135
  return lowerer.lowerRoots(restricted.definition, restricted.kind);
3109
4136
  }
4137
+ function isCellColumn(meta, name) {
4138
+ if (!meta.cells) return false;
4139
+ return Object.values(meta.cells).some((parts) => Object.values(parts).includes(name));
4140
+ }
4141
+ function fieldNodes(selectionSet) {
4142
+ const out = [];
4143
+ for (const node of selectionSet.selections) {
4144
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
4145
+ if (node.directives && node.directives.length > 0) fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
4146
+ if (node.name.value !== "__typename") out.push(node);
4147
+ }
4148
+ return out;
4149
+ }
3110
4150
  function parseSdl(sdl) {
3111
4151
  return graphql.buildSchema(sdl);
3112
4152
  }