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