@sequenceholdings/orm 2.0.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 +94 -2
  2. package/dist/authoring.cjs +1474 -54
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +242 -4
  5. package/dist/authoring.d.ts +242 -4
  6. package/dist/authoring.js +1465 -56
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +740 -42
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +742 -53
  11. package/dist/canonical.d.ts +742 -53
  12. package/dist/canonical.js +724 -43
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +2478 -328
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +2480 -330
  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 +1755 -329
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +56 -3
  27. package/dist/compiler.d.ts +56 -3
  28. package/dist/compiler.js +1756 -330
  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 +1973 -164
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +474 -100
  37. package/dist/index.d.ts +474 -100
  38. package/dist/index.js +1940 -167
  39. package/dist/index.js.map +1 -1
  40. package/dist/ir.cjs +869 -46
  41. package/dist/ir.cjs.map +1 -1
  42. package/dist/ir.d.cts +699 -25
  43. package/dist/ir.d.ts +699 -25
  44. package/dist/ir.js +861 -47
  45. package/dist/ir.js.map +1 -1
  46. package/dist/{naming-CTvWSRTf.d.ts → naming-CPfx7EfF.d.ts} +28 -1
  47. package/dist/{naming-DRyinU4u.d.cts → naming-CU9ThP_E.d.cts} +28 -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 +852 -49
  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 +852 -49
  55. package/dist/predicate-v2.js.map +1 -1
  56. package/dist/sdl.cjs +1309 -58
  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 +1305 -59
  61. package/dist/sdl.js.map +1 -1
  62. package/package.json +11 -6
  63. package/templates/orm-namespace/CLAUDE.md +6 -4
  64. package/templates/orm-namespace/codegen.preset.d.cts +7 -0
  65. package/templates/orm-namespace/codegen.ts +2 -1
  66. package/templates/orm-namespace/graphql/example/queries.ts +4 -3
  67. package/templates/orm-namespace/package.json +2 -1
  68. package/templates/orm-namespace/schema/tables.ts +3 -2
  69. package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/compiler.cjs CHANGED
@@ -7,9 +7,26 @@ var graphql = require('graphql');
7
7
  // src/compiler-gql/artifact.ts
8
8
 
9
9
  // src/constants.ts
10
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
10
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
11
+ var MAX_IDENTIFIER_LENGTH = 63;
12
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
11
13
  var ORM_SCHEMA_PREFIX = "orm_";
12
14
  var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
15
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
16
+ var CELL_PART_SUFFIX = {
17
+ at: "__at",
18
+ by: "__by",
19
+ rev: "__rev",
20
+ source: "__src",
21
+ conflicts: "__lost"
22
+ };
23
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
24
+ var CELLS_FIELD = "cells";
25
+ function cellColumnName(column, part) {
26
+ return `${column}${CELL_PART_SUFFIX[part]}`;
27
+ }
28
+ var MATERIALIZE_ACTOR = "orm:materialize";
29
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
13
30
 
14
31
  // src/hash.ts
15
32
  function stableJson(value) {
@@ -18,9 +35,469 @@ function stableJson(value) {
18
35
  const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
19
36
  return `{${entries.join(",")}}`;
20
37
  }
38
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
39
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
40
+
41
+ // src/checks/compile.ts
42
+ function tokenize(source) {
43
+ const tokens = [];
44
+ for (let position = 0; position < source.length; ) {
45
+ const char = source[position];
46
+ if (/\s/.test(char)) {
47
+ position++;
48
+ continue;
49
+ }
50
+ if (char === "(" || char === ")" || char === ",") {
51
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
52
+ position++;
53
+ continue;
54
+ }
55
+ if (char === "'") {
56
+ const start = position++;
57
+ let value = "";
58
+ let closed = false;
59
+ while (position < source.length) {
60
+ if (source[position] !== "'") {
61
+ value += source[position++];
62
+ continue;
63
+ }
64
+ if (source[position + 1] === "'") {
65
+ value += "'";
66
+ position += 2;
67
+ continue;
68
+ }
69
+ position++;
70
+ closed = true;
71
+ break;
72
+ }
73
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
74
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
75
+ tokens.push({ kind: "string", value, position: start });
76
+ continue;
77
+ }
78
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
79
+ if (operator) {
80
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
81
+ position += operator[0].length;
82
+ continue;
83
+ }
84
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
85
+ if (number) {
86
+ tokens.push({ kind: "number", value: number[0], position });
87
+ position += number[0].length;
88
+ continue;
89
+ }
90
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
91
+ if (identifier3) {
92
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
93
+ position += identifier3[0].length;
94
+ continue;
95
+ }
96
+ throw new Error(`unexpected character '${char}' at position ${position}`);
97
+ }
98
+ tokens.push({ kind: "end", value: "", position: source.length });
99
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
100
+ return tokens;
101
+ }
102
+ var Parser = class {
103
+ constructor(tokens, columns) {
104
+ this.tokens = tokens;
105
+ this.columns = /* @__PURE__ */ new Map();
106
+ for (const column of columns) {
107
+ for (const alias of [column.name, column.columnName]) {
108
+ const previous = this.columns.get(alias);
109
+ if (previous && previous.columnName !== column.columnName) {
110
+ throw new Error(`ambiguous check column '${alias}'`);
111
+ }
112
+ this.columns.set(alias, column);
113
+ }
114
+ }
115
+ }
116
+ index = 0;
117
+ depth = 0;
118
+ columns;
119
+ parse() {
120
+ const expression = this.parseOr();
121
+ this.expect("end", "end of expression");
122
+ return expression;
123
+ }
124
+ parseOr() {
125
+ let left = this.parseAnd();
126
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
127
+ return left;
128
+ }
129
+ parseAnd() {
130
+ let left = this.parseNot();
131
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
132
+ return left;
133
+ }
134
+ parseNot() {
135
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
136
+ return this.parsePrimary();
137
+ }
138
+ parsePrimary() {
139
+ if (this.peek().kind === "lparen") {
140
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
141
+ this.advance();
142
+ const expression = this.parseOr();
143
+ this.expect("rparen", "')'");
144
+ this.depth--;
145
+ return `(${expression})`;
146
+ }
147
+ return this.parseComparison();
148
+ }
149
+ parseComparison() {
150
+ const left = this.parseValue();
151
+ if (this.matchKeyword("is")) {
152
+ const negate = this.matchKeyword("not");
153
+ this.expectKeyword("null");
154
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
155
+ }
156
+ if (this.matchKeyword("between")) {
157
+ const lower = this.parseValue();
158
+ this.expectKeyword("and");
159
+ const upper = this.parseValue();
160
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
161
+ }
162
+ if (this.matchKeyword("in")) {
163
+ this.expect("lparen", "'(' after IN");
164
+ const values = [this.parseValue()];
165
+ while (this.peek().kind === "comma") {
166
+ this.advance();
167
+ values.push(this.parseValue());
168
+ }
169
+ this.expect("rparen", "')' after IN values");
170
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
171
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
172
+ }
173
+ const operator = this.expect("operator", "a comparison operator");
174
+ const right = this.parseValue();
175
+ if (operator.value === "~" || operator.value === "!~") {
176
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
177
+ if (!right.literal) throw new Error("regex pattern must be a literal");
178
+ }
179
+ return `${left.sql} ${operator.value} ${right.sql}`;
180
+ }
181
+ parseValue() {
182
+ const token = this.advance();
183
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
184
+ if (token.kind === "number") {
185
+ const number = Number(token.value);
186
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
187
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
188
+ }
189
+ return { sql: token.value, literal: true };
190
+ }
191
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
192
+ const keyword = token.value.toLowerCase();
193
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
194
+ if (keyword === "btrim") {
195
+ this.expect("lparen", "'(' after btrim");
196
+ const column2 = this.parseColumn();
197
+ this.expect("rparen", "')' after btrim column");
198
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
199
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
200
+ }
201
+ const column = this.columns.get(token.value);
202
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
203
+ return { sql: quoteIdentifier(column.columnName), column };
204
+ }
205
+ parseColumn() {
206
+ const token = this.expect("identifier", "a column name");
207
+ const column = this.columns.get(token.value);
208
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
209
+ return column;
210
+ }
211
+ peek() {
212
+ return this.tokens[this.index];
213
+ }
214
+ advance() {
215
+ const token = this.peek();
216
+ if (token.kind !== "end") this.index++;
217
+ return token;
218
+ }
219
+ matchKeyword(keyword) {
220
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
221
+ this.advance();
222
+ return true;
223
+ }
224
+ expectKeyword(keyword) {
225
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
226
+ }
227
+ expect(kind, description) {
228
+ const token = this.peek();
229
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
230
+ return this.advance();
231
+ }
232
+ };
233
+ function compileCheckExpression(source, columns) {
234
+ return new Parser(tokenize(source), columns).parse();
235
+ }
236
+ var policyVariantSchema = zod.z.object({
237
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
238
+ when: zod.z.string().min(1).max(1e3),
239
+ /** Exactly one of: allow-all bypass, or a row predicate. */
240
+ allow: zod.z.literal(true).optional(),
241
+ policy: zod.z.string().min(1).max(2e3).optional()
242
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
243
+ message: "A variant declares exactly one of allow: true or policy"
244
+ });
245
+ var policyExpressionSchema = zod.z.union([
246
+ zod.z.string().min(1).max(2e3),
247
+ zod.z.object({
248
+ default: zod.z.string().min(1).max(2e3),
249
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
250
+ })
251
+ ]);
252
+ function parseDurationMs(input) {
253
+ if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
254
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
255
+ if (!match) return void 0;
256
+ const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
257
+ const value = Math.round(Number(match[1]) * factor);
258
+ return value > 0 ? value : void 0;
259
+ }
260
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
261
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
262
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
263
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
264
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
265
+ };
266
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
267
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
268
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
269
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
270
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
271
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
272
+ };
273
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
274
+ /* @__PURE__ */ new Set([
275
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
276
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
277
+ ]);
278
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
279
+ var canonicalFunctionRefSchema = zod.z.object({
280
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
281
+ kind: zod.z.enum(["managed", "native"]),
282
+ id: zod.z.string().min(1).max(200)
283
+ }).strict();
284
+ var canonicalFetchShapeSchema = zod.z.union([
285
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
286
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
287
+ ]);
288
+ var canonicalResolvedSourceSchema = zod.z.object({
289
+ name: identifier,
290
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
291
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
292
+ table: identifier,
293
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
294
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
295
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
296
+ updatedAt: zod.z.string().min(1).max(63).optional(),
297
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
298
+ key: zod.z.string().min(1).max(63).optional(),
299
+ fetch: canonicalFetchShapeSchema.optional(),
300
+ function: canonicalFunctionRefSchema.optional(),
301
+ prefetchDefaults: zod.z.object({
302
+ maxStalenessMs: durationMs,
303
+ timeoutMs: durationMs,
304
+ onError: zod.z.enum(["fallback", "fail"])
305
+ }).strict().optional(),
306
+ writeback: zod.z.object({
307
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
308
+ defaultMode: zod.z.enum(["sync", "async"]),
309
+ timeoutMs: durationMs,
310
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
311
+ clearManualOnConfirm: zod.z.boolean()
312
+ }).strict().optional()
313
+ }).strict();
314
+ var canonicalResolveRuleSchema = zod.z.object({
315
+ column: zod.z.string().min(1).max(63),
316
+ comparator: zod.z.enum(COMPARATORS),
317
+ /** Source names, in tie-break order. */
318
+ candidates: zod.z.array(identifier).min(1).max(16),
319
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
320
+ orderBy: zod.z.string().min(1).max(2e3).optional()
321
+ }).strict();
322
+ var canonicalResolvedSchema = zod.z.object({
323
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
324
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
325
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
326
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
327
+ deletedBy: zod.z.array(identifier).max(1).default([]),
328
+ /**
329
+ * Materialized: the view reads an indexed internal table (`table`) that the
330
+ * data plane keeps in step with `compute`, the internal view holding the
331
+ * resolution query. Omitted when the view computes on read.
332
+ */
333
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
334
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
335
+ policies: zod.z.object({
336
+ write: policyExpressionSchema.optional(),
337
+ insert: policyExpressionSchema.optional(),
338
+ update: policyExpressionSchema.optional()
339
+ }).strict().default({})
340
+ }).strict();
341
+ function validateResolvedView({ view, tables, views = [] }) {
342
+ const resolved = view.resolved;
343
+ if (!resolved) return [];
344
+ const errors = [];
345
+ const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
346
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
347
+ if (!pkColumn) {
348
+ fail3("requires exactly one primary-key column");
349
+ return errors;
350
+ }
351
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
352
+ const scope = {
353
+ view,
354
+ resolved,
355
+ pkColumn,
356
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
357
+ sourceByName: /* @__PURE__ */ new Map(),
358
+ fail: fail3
359
+ };
360
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail3("requires exactly one manual source");
361
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
362
+ for (const source of resolved.sources) {
363
+ if (scope.sourceByName.has(source.name)) fail3(`duplicate source '${source.name}'`);
364
+ scope.sourceByName.set(source.name, source);
365
+ const table = tableByName.get(source.table);
366
+ if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
367
+ else validateSource({ scope, source, table });
368
+ }
369
+ validateRules(scope);
370
+ validateViewCells(scope);
371
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
372
+ for (const name of resolved.deletedBy) {
373
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail3(`deletedBy '${name}' must be the manual source`);
374
+ }
375
+ return errors;
376
+ }
377
+ function validateSource({ scope, source, table }) {
378
+ const { fail: fail3 } = scope;
379
+ if (source.kind === "etl" && table.source === void 0) fail3(`etl source '${source.name}' must reference a synced table`);
380
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
381
+ fail3(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
382
+ }
383
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
384
+ if (source.kind === "manual" && !hasRev) fail3(`manual source '${source.name}' must carry '__rev' cell columns`);
385
+ if (source.kind === "prefetch" && hasRev) fail3(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
386
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
387
+ validateSourceColumns({ scope, source, byName });
388
+ if (source.kind === "etl") {
389
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
390
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
391
+ fail3(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
392
+ }
393
+ }
394
+ validatePrefetchFields({ scope, source });
395
+ }
396
+ function validatePrefetchFields({ scope, source }) {
397
+ const { fail: fail3, pkColumn } = scope;
398
+ if (source.kind !== "prefetch") {
399
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
400
+ fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
401
+ }
402
+ return;
403
+ }
404
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
405
+ fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
406
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
407
+ fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
408
+ }
409
+ for (const column of source.writeback?.columns ?? []) {
410
+ if (!source.columns.includes(column)) fail3(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
411
+ }
412
+ }
413
+ function validateSourceColumns({ scope, source, byName }) {
414
+ const { fail: fail3, pkColumn, dataColumns } = scope;
415
+ for (const column of [pkColumn.name, ...source.columns]) {
416
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
417
+ const theirs = byName.get(column);
418
+ if (!own) fail3(`source '${source.name}' supplies unknown column '${column}'`);
419
+ else if (!theirs) fail3(`source '${source.name}' table '${source.table}' has no column '${column}'`);
420
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail3(`column '${column}' has a different type on source '${source.name}'`);
421
+ }
422
+ }
423
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail3 }) {
424
+ const ruled = /* @__PURE__ */ new Set();
425
+ for (const rule of resolved.resolve) {
426
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
427
+ fail3(`resolve rule targets unknown column '${rule.column}'`);
428
+ continue;
429
+ }
430
+ if (ruled.has(rule.column)) fail3(`column '${rule.column}' has more than one resolve rule`);
431
+ ruled.add(rule.column);
432
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
433
+ fail3(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
434
+ }
435
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail3(`column '${rule.column}' repeats a candidate`);
436
+ for (const candidate of rule.candidates) {
437
+ const source = sourceByName.get(candidate);
438
+ if (!source) fail3(`column '${rule.column}' names unknown candidate source '${candidate}'`);
439
+ else if (!source.columns.includes(rule.column)) fail3(`column '${rule.column}' candidate '${candidate}' does not supply it`);
440
+ }
441
+ }
442
+ for (const column of dataColumns.keys()) {
443
+ if (column !== pkColumn.name && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
444
+ }
445
+ }
446
+ var VIEW_CELL_TYPES = {
447
+ source: "text",
448
+ at: "timestamptz",
449
+ by: "text",
450
+ conflicts: "jsonb"
451
+ };
452
+ function validateViewCells({ view, dataColumns, pkColumn, fail: fail3 }) {
453
+ const expected = /* @__PURE__ */ new Map();
454
+ for (const column of dataColumns.values()) {
455
+ if (column.name === pkColumn.name) continue;
456
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
457
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
458
+ }
459
+ }
460
+ for (const column of view.columns) {
461
+ if (!column.cell) continue;
462
+ const want = expected.get(column.name);
463
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
464
+ fail3(`unexpected cell column '${column.name}'`);
465
+ continue;
466
+ }
467
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail3(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
468
+ expected.delete(column.name);
469
+ }
470
+ for (const name of expected.keys()) fail3(`missing cell column '${name}'`);
471
+ }
472
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
473
+ function validateMaterialized({ scope, tableByName, views }) {
474
+ const { view, resolved, fail: fail3 } = scope;
475
+ const { table: tableName, compute: computeName } = resolved.materialized;
476
+ const table = tableByName.get(tableName);
477
+ if (!table || table.internal !== true || table.materializes !== view.view) {
478
+ fail3(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
479
+ } else {
480
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail3(`materialized table '${tableName}' must have exactly the view's columns`);
481
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail3(`materialized table '${tableName}' must share the view's primary key`);
482
+ }
483
+ const compute = views.find((candidate) => candidate.view === computeName);
484
+ if (!compute || compute.internal !== true) fail3(`compute view '${computeName}' must be an internal view`);
485
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail3(`compute view '${computeName}' must have exactly the view's columns`);
486
+ for (const source of resolved.sources) {
487
+ if (source.kind !== "etl") continue;
488
+ const read = tableByName.get(source.table)?.policies?.read;
489
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
490
+ if (!unconditional) {
491
+ fail3(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
492
+ }
493
+ }
494
+ }
495
+
496
+ // src/canonical/v2.ts
21
497
  var CANONICAL_FORMAT = 2;
22
498
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
23
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
499
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
500
+ var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
24
501
  var COLUMN_TYPES = [
25
502
  "uuid",
26
503
  "text",
@@ -35,59 +512,71 @@ var COLUMN_TYPES = [
35
512
  ];
36
513
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
37
514
  var canonicalEnumSchema = zod.z.object({
38
- name: identifier,
515
+ name: identifier2,
39
516
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
40
517
  });
41
518
  var canonicalColumnSchema = zod.z.object({
42
519
  /** Property name in TS (camelCase); columnName is the Postgres name. */
43
520
  name: zod.z.string().min(1).max(63),
44
- columnName: identifier,
45
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
521
+ columnName: columnIdentifier,
522
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
46
523
  nullable: zod.z.boolean().default(true),
47
524
  unique: zod.z.boolean().default(false),
48
525
  /** Literal default, or one of the allowlisted SQL functions. */
49
526
  default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
50
527
  defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
528
+ /**
529
+ * Stamp this column from the database clock on UPDATE (`now()`). The
530
+ * column stays insertable (defaults fill gaps) but is omitted from
531
+ * SetInput / settable so callers cannot backdate it.
532
+ */
533
+ onUpdateFn: zod.z.enum(["now()"]).optional(),
51
534
  description: zod.z.string().max(500).optional(),
52
535
  /** Migration hints — same semantics as v1. */
53
- renamedFrom: identifier.optional(),
536
+ renamedFrom: columnIdentifier.optional(),
54
537
  cast: zod.z.string().max(200).optional(),
55
- deprecated: zod.z.boolean().default(false)
538
+ deprecated: zod.z.boolean().default(false),
539
+ /**
540
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
541
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
542
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
543
+ * ordinary columns so their hashes are unchanged.
544
+ */
545
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
546
+ }).superRefine((column, ctx) => {
547
+ if (column.onUpdateFn === void 0) return;
548
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
549
+ ctx.addIssue({
550
+ code: "custom",
551
+ path: ["onUpdateFn"],
552
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
553
+ });
554
+ }
56
555
  });
57
556
  var canonicalLinkSchema = zod.z.object({
58
557
  name: zod.z.string().min(1).max(63),
59
558
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
60
559
  /** Target table name (namespace-local). Absent for toPlatformUser. */
61
- target: identifier.optional(),
560
+ target: identifier2.optional(),
62
561
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
63
- on: identifier,
562
+ on: columnIdentifier,
64
563
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
65
- references: identifier.optional(),
564
+ references: columnIdentifier.optional(),
66
565
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
67
566
  enforce: zod.z.boolean().default(false),
567
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
568
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
68
569
  description: zod.z.string().max(500).optional()
69
570
  });
70
571
  var canonicalIndexSchema = zod.z.object({
71
- columns: zod.z.array(identifier).min(1),
572
+ columns: zod.z.array(columnIdentifier).min(1),
72
573
  unique: zod.z.boolean().default(false),
73
- name: identifier.optional()
574
+ name: identifier2.optional()
74
575
  });
75
- var policyVariantSchema = zod.z.object({
76
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
77
- when: zod.z.string().min(1).max(1e3),
78
- /** Exactly one of: allow-all bypass, or a row predicate. */
79
- allow: zod.z.literal(true).optional(),
80
- policy: zod.z.string().min(1).max(2e3).optional()
81
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
82
- message: "A variant declares exactly one of allow: true or policy"
83
- });
84
- var policyExpressionSchema = zod.z.union([
85
- zod.z.string().min(1).max(2e3),
86
- zod.z.object({
87
- default: zod.z.string().min(1).max(2e3),
88
- variants: zod.z.array(policyVariantSchema).max(10).default([])
89
- })
90
- ]);
576
+ var canonicalCheckSchema = zod.z.object({
577
+ name: identifier2,
578
+ expression: zod.z.string().min(1).max(2e3)
579
+ }).strict();
91
580
  var canonicalPoliciesSchema = zod.z.object({
92
581
  read: policyExpressionSchema.optional(),
93
582
  write: policyExpressionSchema.optional(),
@@ -118,7 +607,7 @@ function isSingleViewQuerySql(value) {
118
607
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
119
608
  }
120
609
  var canonicalActionSchema = zod.z.object({
121
- name: identifier,
610
+ name: identifier2,
122
611
  requires: zod.z.string().max(1e3).optional(),
123
612
  params: zod.z.record(zod.z.string(), zod.z.object({
124
613
  type: columnTypeSchema,
@@ -130,9 +619,9 @@ var canonicalActionSchema = zod.z.object({
130
619
  });
131
620
  var canonicalCowMutationSchema = zod.z.object({
132
621
  kind: zod.z.enum(["override", "revert"]),
133
- name: identifier,
622
+ name: identifier2,
134
623
  /** Synced table (same namespace) the overlay copies from / falls back to. */
135
- synced: identifier,
624
+ synced: identifier2,
136
625
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
137
626
  pk: zod.z.string().min(1).max(63),
138
627
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -159,17 +648,28 @@ var canonicalCowMutationSchema = zod.z.object({
159
648
  }
160
649
  }
161
650
  });
651
+ var canonicalForeignKeySchema = zod.z.object({
652
+ name: identifier2,
653
+ columns: zod.z.array(columnIdentifier).min(1).max(8),
654
+ target: identifier2,
655
+ references: zod.z.array(columnIdentifier).min(1).max(8),
656
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
657
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
658
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
659
+ }).strict();
162
660
  var canonicalTableSchema = zod.z.object({
163
- table: identifier,
661
+ table: identifier2,
164
662
  description: zod.z.string().max(2e3).optional(),
165
663
  /** Table-level rename hint (new in v2). */
166
- renamedFrom: identifier.optional(),
664
+ renamedFrom: identifier2.optional(),
167
665
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
168
666
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
169
667
  columns: zod.z.array(canonicalColumnSchema).default([]),
170
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
668
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
171
669
  links: zod.z.array(canonicalLinkSchema).default([]),
172
670
  indexes: zod.z.array(canonicalIndexSchema).default([]),
671
+ /** Omitted by old definitions, preserving their existing content hashes. */
672
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
173
673
  policies: canonicalPoliciesSchema.default({}),
174
674
  /**
175
675
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -178,26 +678,64 @@ var canonicalTableSchema = zod.z.object({
178
678
  */
179
679
  actions: zod.z.array(canonicalActionSchema).default([]),
180
680
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
181
- cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
681
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
682
+ /** Omitted by old definitions, preserving their existing content hashes. */
683
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
684
+ /**
685
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
686
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
687
+ * on a resolved table's internal sources), listed explicitly in `columns`
688
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
689
+ * Omitted when off so existing definitions hash unchanged.
690
+ */
691
+ cellMetadata: zod.z.literal(true).optional(),
692
+ /**
693
+ * System-owned backing table of a resolved table (its manual or prefetch
694
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
695
+ * only the data plane writes them. Omitted when false.
696
+ */
697
+ internal: zod.z.literal(true).optional(),
698
+ /**
699
+ * The materialized copy of resolved table `materializes` (internal): the
700
+ * same columns as that view, including its cell parts, maintained by the
701
+ * data plane. Omitted otherwise.
702
+ */
703
+ materializes: identifier2.optional()
182
704
  });
705
+ var MAX_VIEW_QUERY = 1e5;
706
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
183
707
  var canonicalViewSchema = zod.z.object({
184
- view: identifier,
708
+ view: identifier2,
185
709
  description: zod.z.string().max(2e3).optional(),
186
710
  columns: zod.z.array(canonicalColumnSchema).min(1),
187
711
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
188
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
712
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
189
713
  links: zod.z.array(canonicalLinkSchema).default([]),
190
714
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
191
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
715
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
192
716
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
193
717
  }),
194
718
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
195
- dependencies: zod.z.array(identifier).max(500)
719
+ dependencies: zod.z.array(identifier2).max(500),
720
+ /**
721
+ * Present when this view is a resolved table (defineResolvedTable): the
722
+ * multi-source metadata behind its generated query. Omitted otherwise so
723
+ * existing views hash unchanged.
724
+ */
725
+ resolved: canonicalResolvedSchema.optional(),
726
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
727
+ internal: zod.z.literal(true).optional()
196
728
  }).superRefine((view, ctx) => {
729
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
730
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
731
+ }
197
732
  for (const [index, column] of view.columns.entries()) {
198
733
  if (column.default !== void 0 || column.defaultFn !== void 0) {
199
734
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
200
735
  }
736
+ if (column.onUpdateFn !== void 0) {
737
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
738
+ }
201
739
  if (column.unique) {
202
740
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
203
741
  }
@@ -205,15 +743,82 @@ var canonicalViewSchema = zod.z.object({
205
743
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
206
744
  }
207
745
  }
746
+ if (view.resolved === void 0 && view.internal !== true) {
747
+ for (const [index, column] of view.columns.entries()) {
748
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
749
+ }
750
+ }
208
751
  for (const [index, link] of view.links.entries()) {
209
752
  if (link.enforce) {
210
753
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
211
754
  }
755
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
756
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
757
+ }
212
758
  }
213
759
  });
760
+ var CELL_PART_TYPES = {
761
+ at: "timestamptz",
762
+ by: "text",
763
+ rev: "uuid",
764
+ source: "text",
765
+ conflicts: "jsonb"
766
+ };
767
+ function trackedCellColumns(table) {
768
+ const pk = new Set(table.primaryKey ?? []);
769
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
770
+ }
771
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
772
+ function validateCellMetadata(table, index, ctx) {
773
+ const fail3 = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
774
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
775
+ if (table.materializes !== void 0) return;
776
+ if (table.cellMetadata !== true) {
777
+ for (const { position } of cells) fail3(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
778
+ return;
779
+ }
780
+ if (table.source !== void 0) {
781
+ fail3(`synced table '${table.table}' cannot track cell metadata`);
782
+ return;
783
+ }
784
+ const tracked = trackedCellColumns(table);
785
+ const byName = new Map(tracked.map((column) => [column.name, column]));
786
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
787
+ if (withRev && table.internal !== true) fail3(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
788
+ const seen = /* @__PURE__ */ new Set();
789
+ for (const { column, position, cell } of cells) {
790
+ const path = ["tables", index, "columns", position];
791
+ const owner = byName.get(cell.of);
792
+ if (!owner) {
793
+ fail3(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
794
+ continue;
795
+ }
796
+ if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail3(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
797
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
798
+ fail3(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
799
+ }
800
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
801
+ fail3(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
802
+ }
803
+ const key = `${cell.of}:${cell.part}`;
804
+ if (seen.has(key)) fail3(`duplicate cell column '${column.name}'`, path);
805
+ seen.add(key);
806
+ }
807
+ for (const column of tracked) {
808
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
809
+ fail3(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
810
+ }
811
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
812
+ fail3(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
813
+ }
814
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
815
+ if (!seen.has(`${column.name}:${part}`)) fail3(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
816
+ }
817
+ }
818
+ }
214
819
  var canonicalDefinitionSchema = zod.z.object({
215
820
  definition_format: zod.z.literal(CANONICAL_FORMAT),
216
- namespace: identifier.max(41),
821
+ namespace: identifier2.max(41),
217
822
  description: zod.z.string().max(2e3).optional(),
218
823
  enums: zod.z.array(canonicalEnumSchema).default([]),
219
824
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
@@ -231,6 +836,74 @@ var canonicalDefinitionSchema = zod.z.object({
231
836
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
232
837
  }
233
838
  seen.add(table.table);
839
+ validateCellMetadata(table, index, ctx);
840
+ if (table.source !== void 0) {
841
+ for (const [columnIndex, column] of table.columns.entries()) {
842
+ if (column.onUpdateFn !== void 0) {
843
+ ctx.addIssue({
844
+ code: "custom",
845
+ path: ["tables", index, "columns", columnIndex],
846
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
847
+ });
848
+ }
849
+ }
850
+ }
851
+ }
852
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
853
+ for (const [tableIndex, table] of definition.tables.entries()) {
854
+ const names = /* @__PURE__ */ new Set([
855
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
856
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
857
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
858
+ ]);
859
+ for (const [index, link] of table.links.entries()) {
860
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
861
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
862
+ }
863
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
864
+ const source = table.columns.find((column) => column.columnName === link.on);
865
+ if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "SET NULL requires a nullable source column" });
866
+ }
867
+ }
868
+ for (const [index, check] of (table.checks ?? []).entries()) {
869
+ const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
870
+ if (table.source) fail3("synced tables cannot declare checks");
871
+ if (names.has(check.name)) fail3(`duplicate constraint '${check.name}'`);
872
+ names.add(check.name);
873
+ try {
874
+ compileCheckExpression(check.expression, table.columns);
875
+ } catch (error) {
876
+ fail3(error instanceof Error ? error.message : String(error));
877
+ }
878
+ }
879
+ for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
880
+ const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
881
+ if (names.has(fk.name)) fail3(`duplicate foreign key '${fk.name}'`);
882
+ names.add(fk.name);
883
+ const target = fkTables.get(fk.target);
884
+ if (table.source || !target || target.source) {
885
+ fail3("foreign keys must connect owned tables in the same namespace");
886
+ continue;
887
+ }
888
+ if (fk.columns.length !== fk.references.length) fail3("foreign key columns and references must have the same number of columns");
889
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail3("foreign key columns must not repeat");
890
+ for (const [position, name] of fk.columns.entries()) {
891
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
892
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
893
+ if (!sourceColumn || !targetColumn) fail3("foreign key references an unknown column");
894
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail3("foreign key column types must match");
895
+ }
896
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
897
+ if (fk.columns.some((name) => table.columns.find((column) => column.columnName === name)?.nullable === false || table.primaryKey?.includes(name))) fail3("SET NULL requires nullable source columns");
898
+ }
899
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
900
+ if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail3("foreign key target columns must form a unique key");
901
+ }
902
+ }
903
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
904
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
905
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
906
+ }
234
907
  }
235
908
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
236
909
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -326,7 +999,7 @@ var canonicalDefinitionSchema = zod.z.object({
326
999
  const visiting = /* @__PURE__ */ new Set();
327
1000
  const visited = /* @__PURE__ */ new Set();
328
1001
  let cycleReported = false;
329
- const visit = (name, path) => {
1002
+ const visit2 = (name, path) => {
330
1003
  if (visited.has(name) || cycleReported) return;
331
1004
  if (visiting.has(name)) {
332
1005
  cycleReported = true;
@@ -340,12 +1013,12 @@ var canonicalDefinitionSchema = zod.z.object({
340
1013
  visiting.add(name);
341
1014
  const view = viewByName.get(name);
342
1015
  for (const dependency of view?.dependencies ?? []) {
343
- if (viewByName.has(dependency)) visit(dependency, [...path, name]);
1016
+ if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
344
1017
  }
345
1018
  visiting.delete(name);
346
1019
  visited.add(name);
347
1020
  };
348
- for (const view of views) visit(view.view, []);
1021
+ for (const view of views) visit2(view.view, []);
349
1022
  });
350
1023
  function canonicalRelations(definition) {
351
1024
  const views = (definition.views ?? []).map((view) => ({
@@ -355,8 +1028,12 @@ function canonicalRelations(definition) {
355
1028
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
356
1029
  links: view.links,
357
1030
  indexes: [],
358
- policies: { read: view.policies.read },
359
- actions: []
1031
+ // A resolved table's write policies (evaluated against the resolved row)
1032
+ // ride alongside the view's read policy.
1033
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
1034
+ actions: [],
1035
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
1036
+ ...view.internal ? { internal: true } : {}
360
1037
  }));
361
1038
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
362
1039
  }
@@ -366,12 +1043,14 @@ function normalizeCanonical(definition) {
366
1043
  ...definition,
367
1044
  enums: sortByName(definition.enums),
368
1045
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
369
- const { cowMutations, ...rest } = t;
1046
+ const { cowMutations, foreignKeys, checks, ...rest } = t;
370
1047
  return {
371
1048
  ...rest,
372
1049
  links: sortByName(t.links),
373
1050
  indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
374
1051
  actions: sortByName(t.actions),
1052
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1053
+ ...checks?.length ? { checks: sortByName(checks) } : {},
375
1054
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
376
1055
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
377
1056
  };
@@ -388,7 +1067,18 @@ function normalizeCanonical(definition) {
388
1067
  }
389
1068
 
390
1069
  // src/ir/index.ts
391
- var IR_VERSION = 1;
1070
+ var IR_VERSION = 2;
1071
+ var LEGACY_IR_VERSION = 1;
1072
+ var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
1073
+ var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
1074
+ function requiredArtifactIrVersion(operations) {
1075
+ let required = LEGACY_IR_VERSION;
1076
+ for (const operation of operations) {
1077
+ const { irVersion } = operationVersionSchema.parse(operation);
1078
+ if (irVersion > required) required = irVersion;
1079
+ }
1080
+ return required;
1081
+ }
392
1082
  var CLAIMS_CONTRACT = {
393
1083
  sub: "string",
394
1084
  org: "string?",
@@ -460,6 +1150,21 @@ var selectionIrSchema = zod.z.lazy(
460
1150
  () => zod.z.object({
461
1151
  typename: zod.z.string(),
462
1152
  columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
1153
+ cells: zod.z.object({
1154
+ alias: zod.z.string(),
1155
+ typename: zod.z.string(),
1156
+ fields: zod.z.array(zod.z.object({
1157
+ alias: zod.z.string(),
1158
+ column: zod.z.string(),
1159
+ typename: zod.z.string(),
1160
+ parts: zod.z.array(zod.z.object({
1161
+ alias: zod.z.string(),
1162
+ part: zod.z.enum(["source", "at", "by", "conflicts"]),
1163
+ fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
1164
+ typename: zod.z.string().optional()
1165
+ }))
1166
+ }))
1167
+ }).optional(),
463
1168
  relations: zod.z.array(zod.z.object({
464
1169
  alias: zod.z.string(),
465
1170
  link: zod.z.string(),
@@ -481,6 +1186,11 @@ var selectionIrSchema = zod.z.lazy(
481
1186
  })
482
1187
  );
483
1188
  var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
1189
+ var prefetchPlanIrSchema = zod.z.object({
1190
+ source: zod.z.string(),
1191
+ maxStalenessMs: zod.z.number().int().positive().optional(),
1192
+ key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
1193
+ });
484
1194
  var selectRootSchema = zod.z.object({
485
1195
  kind: zod.z.literal("select"),
486
1196
  alias: zod.z.string(),
@@ -492,7 +1202,8 @@ var selectRootSchema = zod.z.object({
492
1202
  distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
493
1203
  limit: limitIrSchema,
494
1204
  offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
495
- includeIf: includeIfSchema.optional()
1205
+ includeIf: includeIfSchema.optional(),
1206
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
496
1207
  });
497
1208
  var byPkRootSchema = zod.z.object({
498
1209
  kind: zod.z.literal("byPk"),
@@ -502,7 +1213,8 @@ var byPkRootSchema = zod.z.object({
502
1213
  pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
503
1214
  /** Policy predicate — ANDed with the PK match. */
504
1215
  where: predIrSchema,
505
- includeIf: includeIfSchema.optional()
1216
+ includeIf: includeIfSchema.optional(),
1217
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
506
1218
  });
507
1219
  var aggregateRootSchema = zod.z.object({
508
1220
  kind: zod.z.literal("aggregate"),
@@ -515,6 +1227,33 @@ var aggregateRootSchema = zod.z.object({
515
1227
  nodes: selectionIrSchema.optional(),
516
1228
  includeIf: includeIfSchema.optional()
517
1229
  });
1230
+ var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
1231
+ var groupByKeyIrSchema = zod.z.object({
1232
+ column: zod.z.string(),
1233
+ bucket: zod.z.enum(DATE_BUCKETS).optional()
1234
+ });
1235
+ var aggregateGroupsRootSchema = zod.z.object({
1236
+ kind: zod.z.literal("aggregateGroups"),
1237
+ alias: zod.z.string(),
1238
+ table: zod.z.string(),
1239
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
1240
+ where: predIrSchema,
1241
+ groupBy: zod.z.array(groupByKeyIrSchema).min(1),
1242
+ /**
1243
+ * The `key { … }` projection: which group columns appear in each entry's key
1244
+ * object, in selection order. A subset of `groupBy` (the compiler rejects a
1245
+ * key field that is not a group key — it would be indeterminate within the
1246
+ * group). Empty when the document selects only `aggregate`.
1247
+ */
1248
+ key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
1249
+ count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
1250
+ sum: zod.z.array(zod.z.string()).optional(),
1251
+ avg: zod.z.array(zod.z.string()).optional(),
1252
+ /** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
1253
+ limit: limitIrSchema,
1254
+ offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
1255
+ includeIf: includeIfSchema.optional()
1256
+ });
518
1257
  var writeValuesSchema = zod.z.union([
519
1258
  zod.z.object({ slot: zod.z.string() }),
520
1259
  zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
@@ -533,7 +1272,20 @@ var mutationRootSchema = zod.z.object({
533
1272
  * coalesce view falls back to synced. Both are gated on a matching synced
534
1273
  * row and carry the write policy in `where` like update/delete.
535
1274
  */
536
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
1275
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
1276
+ /**
1277
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
1278
+ * executor gates on the resolved row (write policy in `where`), writes only
1279
+ * the touched cells into the manual source, then re-checks `insertCheck`
1280
+ * against the resolved row. `revert` clears `revertColumns` from the manual
1281
+ * source; `delete` tombstones the row there.
1282
+ */
1283
+ resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
1284
+ revertColumns: zod.z.array(zod.z.string()).optional(),
1285
+ /** `@writeback(source:, mode:)` directives on this root. */
1286
+ writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
1287
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
1288
+ localOnly: zod.z.array(zod.z.string()).optional(),
537
1289
  alias: zod.z.string(),
538
1290
  table: zod.z.string(),
539
1291
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -560,12 +1312,15 @@ var mutationRootSchema = zod.z.object({
560
1312
  insertCheck: predIrSchema.optional(),
561
1313
  returning: selectionIrSchema.nullable(),
562
1314
  affectedRows: zod.z.boolean().default(false),
1315
+ /** v2: abort the transaction when the actual write count differs, regardless of selection. */
1316
+ assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
563
1317
  includeIf: includeIfSchema.optional()
564
1318
  });
565
1319
  var rootIrSchema = zod.z.discriminatedUnion("kind", [
566
1320
  selectRootSchema,
567
1321
  byPkRootSchema,
568
1322
  aggregateRootSchema,
1323
+ aggregateGroupsRootSchema,
569
1324
  mutationRootSchema
570
1325
  ]);
571
1326
  var INPUT_REF_KINDS = [
@@ -595,7 +1350,7 @@ var variableIrSchema = zod.z.object({
595
1350
  defaultValue: variableDefaultIrSchema.optional()
596
1351
  });
597
1352
  var compiledOperationSchema = zod.z.object({
598
- irVersion: zod.z.literal(IR_VERSION),
1353
+ irVersion: irVersionSchema,
599
1354
  name: zod.z.string(),
600
1355
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
601
1356
  kind: zod.z.enum(["query", "mutation"]),
@@ -607,6 +1362,50 @@ var compiledOperationSchema = zod.z.object({
607
1362
  /** Every table the op touches — data-token scoping + audit. */
608
1363
  tableSet: zod.z.array(zod.z.string()).min(1),
609
1364
  roots: zod.z.array(rootIrSchema).min(1)
1365
+ }).superRefine((operation, ctx) => {
1366
+ for (const [index, root] of operation.roots.entries()) {
1367
+ if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
1368
+ if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
1369
+ ctx.addIssue({ code: "custom", path: ["roots", index], message: "affected-row assertions require unconditional mutation roots and IR v2" });
1370
+ }
1371
+ }
1372
+ });
1373
+ var resolvedSourceIrSchema = zod.z.object({
1374
+ name: zod.z.string(),
1375
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
1376
+ /** Logical table key of the backing relation. */
1377
+ table: zod.z.string(),
1378
+ columns: zod.z.array(zod.z.string()),
1379
+ key: zod.z.string().optional(),
1380
+ fetch: zod.z.union([
1381
+ zod.z.object({ shape: zod.z.literal("single") }),
1382
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
1383
+ ]).optional(),
1384
+ function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
1385
+ prefetchDefaults: zod.z.object({
1386
+ maxStalenessMs: zod.z.number().int().positive(),
1387
+ timeoutMs: zod.z.number().int().positive(),
1388
+ onError: zod.z.enum(["fallback", "fail"])
1389
+ }).optional(),
1390
+ writeback: zod.z.object({
1391
+ columns: zod.z.array(zod.z.string()),
1392
+ defaultMode: zod.z.enum(["sync", "async"]),
1393
+ timeoutMs: zod.z.number().int().positive(),
1394
+ clearManualOnConfirm: zod.z.boolean()
1395
+ }).optional()
1396
+ });
1397
+ var resolvedMetaIrSchema = zod.z.object({
1398
+ /** Logical pk column of the resolved view (single-column). */
1399
+ pk: zod.z.string(),
1400
+ /** Source name of the manual source. */
1401
+ manual: zod.z.string(),
1402
+ sources: zod.z.array(resolvedSourceIrSchema),
1403
+ deletedBy: zod.z.array(zod.z.string()),
1404
+ /**
1405
+ * Materialized: the view reads `table`, which the data plane refreshes from
1406
+ * the `compute` view (claims-gated) whenever it writes a source row.
1407
+ */
1408
+ materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
610
1409
  });
611
1410
  var tableMetaIrSchema = zod.z.object({
612
1411
  table: zod.z.string(),
@@ -643,18 +1442,45 @@ var tableMetaIrSchema = zod.z.object({
643
1442
  insertable: zod.z.array(zod.z.string()),
644
1443
  settable: zod.z.array(zod.z.string()),
645
1444
  appendable: zod.z.array(zod.z.string()),
1445
+ /**
1446
+ * Logical columns stamped with Postgres `now()` on UPDATE (canonical
1447
+ * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
1448
+ */
1449
+ touchOnUpdate: zod.z.array(zod.z.string()).optional(),
1450
+ /**
1451
+ * Per-cell metadata: logical column → the logical names of its cell-part
1452
+ * columns (which are listed in `columns`, but never filterable, orderable or
1453
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
1454
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
1455
+ * the relation tracks no cells.
1456
+ */
1457
+ cells: zod.z.record(zod.z.string(), zod.z.object({
1458
+ at: zod.z.string().optional(),
1459
+ by: zod.z.string().optional(),
1460
+ rev: zod.z.string().optional(),
1461
+ source: zod.z.string().optional(),
1462
+ conflicts: zod.z.string().optional()
1463
+ }).strict()).optional(),
1464
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
1465
+ resolved: resolvedMetaIrSchema.optional(),
646
1466
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
647
1467
  /** Depth/node caps for dynamic filter trees on this table. */
648
1468
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
649
1469
  dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
650
1470
  });
651
1471
  var compiledArtifactSchema = zod.z.object({
652
- irVersion: zod.z.literal(IR_VERSION),
1472
+ irVersion: irVersionSchema,
653
1473
  namespace: zod.z.string(),
654
1474
  schemaHash: zod.z.string(),
655
1475
  opsHash: zod.z.string(),
656
1476
  tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
657
1477
  operations: zod.z.record(zod.z.string(), compiledOperationSchema)
1478
+ }).superRefine((artifact, ctx) => {
1479
+ for (const [name, operation] of Object.entries(artifact.operations)) {
1480
+ if (operation.irVersion > artifact.irVersion) {
1481
+ ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
1482
+ }
1483
+ }
658
1484
  });
659
1485
  var IrVersionError = class extends Error {
660
1486
  constructor(found) {
@@ -665,19 +1491,128 @@ var IrVersionError = class extends Error {
665
1491
  function normalizeIr(raw) {
666
1492
  if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
667
1493
  const version = raw.irVersion;
668
- if (version !== IR_VERSION) throw new IrVersionError(version);
1494
+ if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
669
1495
  return compiledArtifactSchema.parse(raw);
670
1496
  }
671
1497
 
1498
+ // src/sdl/naming.ts
1499
+ function camelCase(name) {
1500
+ const segments = name.split("_").filter((segment) => segment.length > 0);
1501
+ const first = segments[0];
1502
+ if (first === void 0) return name;
1503
+ const head = first.charAt(0).toLowerCase() + first.slice(1);
1504
+ return head + segments.slice(1).map(capitalizeFirst).join("");
1505
+ }
1506
+ function pascalCase(name) {
1507
+ return capitalizeFirst(camelCase(name));
1508
+ }
1509
+ function capitalizeFirst(segment) {
1510
+ return segment.charAt(0).toUpperCase() + segment.slice(1);
1511
+ }
1512
+ function singularizeWord(word) {
1513
+ if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
1514
+ for (const suffix of ["sses", "xes", "ches", "shes"]) {
1515
+ if (word.endsWith(suffix)) return word.slice(0, -2);
1516
+ }
1517
+ if (word.endsWith("ss")) return word;
1518
+ if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
1519
+ return word;
1520
+ }
1521
+ function typeName(table) {
1522
+ const segments = table.split("_").filter((segment) => segment.length > 0);
1523
+ const last = segments[segments.length - 1];
1524
+ if (last === void 0) return pascalCase(table);
1525
+ return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
1526
+ }
1527
+ function enumTypeName(name) {
1528
+ return pascalCase(name);
1529
+ }
1530
+ var SCALAR_FOR_COLUMN_TYPE = {
1531
+ uuid: "uuid",
1532
+ text: "String",
1533
+ integer: "Int",
1534
+ bigint: "bigint",
1535
+ numeric: "numeric",
1536
+ double: "Float",
1537
+ boolean: "Boolean",
1538
+ timestamptz: "timestamptz",
1539
+ date: "date",
1540
+ jsonb: "jsonb"
1541
+ };
1542
+ function graphqlScalarFor(type) {
1543
+ return SCALAR_FOR_COLUMN_TYPE[type];
1544
+ }
1545
+ var COLUMN_TYPE_BY_SCALAR = new Map([
1546
+ ["ID", "text"],
1547
+ ...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
1548
+ ]);
1549
+ function columnTypeForScalar(scalarName) {
1550
+ return COLUMN_TYPE_BY_SCALAR.get(scalarName);
1551
+ }
1552
+ var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
1553
+ var PLATFORM_USER_TYPENAME = "PlatformUser";
1554
+ function tableNames(table) {
1555
+ const root = camelCase(table);
1556
+ const rootPascal = pascalCase(table);
1557
+ const type = typeName(table);
1558
+ return {
1559
+ typename: type,
1560
+ selectRoot: root,
1561
+ byPkRoot: `${root}ByPk`,
1562
+ aggregateRoot: `${root}Aggregate`,
1563
+ insertRoot: `insert${rootPascal}`,
1564
+ insertOneRoot: `insert${rootPascal}One`,
1565
+ updateRoot: `update${rootPascal}`,
1566
+ updateByPkRoot: `update${rootPascal}ByPk`,
1567
+ deleteRoot: `delete${rootPascal}`,
1568
+ deleteByPkRoot: `delete${rootPascal}ByPk`,
1569
+ revertByPkRoot: `revert${rootPascal}ByPk`,
1570
+ boolExp: `${type}BoolExp`,
1571
+ orderBy: `${type}OrderBy`,
1572
+ selectColumn: `${type}SelectColumn`,
1573
+ insertInput: `${type}InsertInput`,
1574
+ setInput: `${type}SetInput`,
1575
+ appendInput: `${type}AppendInput`,
1576
+ incInput: `${type}IncInput`,
1577
+ onConflict: `${type}OnConflict`,
1578
+ pkColumnsInput: `${type}PkColumnsInput`,
1579
+ constraintEnum: `${type}Constraint`,
1580
+ updateColumnEnum: `${type}UpdateColumn`,
1581
+ mutationResponse: `${type}MutationResponse`,
1582
+ aggregate: `${type}Aggregate`,
1583
+ aggregateFields: `${type}AggregateFields`,
1584
+ sumFields: `${type}SumFields`,
1585
+ avgFields: `${type}AvgFields`,
1586
+ aggregateBoolExp: `${type}AggregateBoolExp`,
1587
+ aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
1588
+ aggregateGroupsRoot: `${root}AggregateGroups`,
1589
+ groupByColumnEnum: `${type}GroupByColumn`,
1590
+ groupByInput: `${type}GroupBy`,
1591
+ groupKey: `${type}GroupKey`,
1592
+ aggregateGroup: `${type}AggregateGroup`
1593
+ };
1594
+ }
1595
+ var CELL_TYPENAMES = {
1596
+ /** A plain table's cell: when and by whom it was last written. */
1597
+ stamp: "CellStamp",
1598
+ /** A resolved table's cell: which source won, when, by whom, and newer losers. */
1599
+ resolved: "ResolvedCell",
1600
+ /** A newer candidate that lost to the column's rule. */
1601
+ conflict: "CellConflict"
1602
+ };
1603
+ function cellsTypename(typename) {
1604
+ return `${typename}Cells`;
1605
+ }
1606
+
672
1607
  // src/predicate-v2/ast.ts
673
1608
  var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
674
- function walkPolicy(node, visit) {
675
- visit(node);
1609
+ function walkPolicy(node, visit2) {
1610
+ visit2(node);
676
1611
  if (node.kind === "and" || node.kind === "or") {
677
- for (const operand of node.operands) walkPolicy(operand, visit);
1612
+ for (const operand of node.operands) walkPolicy(operand, visit2);
678
1613
  }
679
- if (node.kind === "not") walkPolicy(node.operand, visit);
680
- if (node.kind === "exists") walkPolicy(node.where, visit);
1614
+ if (node.kind === "not") walkPolicy(node.operand, visit2);
1615
+ if (node.kind === "exists") walkPolicy(node.where, visit2);
681
1616
  }
682
1617
 
683
1618
  // src/predicate-v2/errors.ts
@@ -705,7 +1640,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
705
1640
  var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
706
1641
  var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
707
1642
  var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
708
- function tokenize(source) {
1643
+ function tokenize2(source) {
709
1644
  const tokens = [];
710
1645
  let position = 0;
711
1646
  while (position < source.length) {
@@ -784,7 +1719,7 @@ function readString(source, start) {
784
1719
  }
785
1720
  throw new PolicyParseError("unterminated string literal", start);
786
1721
  }
787
- var Parser = class {
1722
+ var Parser2 = class {
788
1723
  constructor(tokens) {
789
1724
  this.tokens = tokens;
790
1725
  }
@@ -958,7 +1893,7 @@ var Parser = class {
958
1893
  function parsePolicy(source) {
959
1894
  const trimmed = source.trim();
960
1895
  if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
961
- return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
1896
+ return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
962
1897
  }
963
1898
 
964
1899
  // src/predicate-v2/compile.ts
@@ -1183,21 +2118,21 @@ function andPred(...preds) {
1183
2118
  if (operands.length === 1) return operands[0];
1184
2119
  return { kind: "and", operands };
1185
2120
  }
1186
- function walkPred(pred, visit) {
1187
- visit(pred);
2121
+ function walkPred(pred, visit2) {
2122
+ visit2(pred);
1188
2123
  switch (pred.kind) {
1189
2124
  case "and":
1190
2125
  case "or":
1191
- for (const operand of pred.operands) walkPred(operand, visit);
2126
+ for (const operand of pred.operands) walkPred(operand, visit2);
1192
2127
  return;
1193
2128
  case "not":
1194
- walkPred(pred.operand, visit);
2129
+ walkPred(pred.operand, visit2);
1195
2130
  return;
1196
2131
  case "exists":
1197
- walkPred(pred.where, visit);
2132
+ walkPred(pred.where, visit2);
1198
2133
  return;
1199
2134
  case "aggPredicate":
1200
- if (pred.where) walkPred(pred.where, visit);
2135
+ if (pred.where) walkPred(pred.where, visit2);
1201
2136
  return;
1202
2137
  default:
1203
2138
  return;
@@ -1222,12 +2157,12 @@ function predContains(haystack, needle) {
1222
2157
  // src/compiler-gql/inject.ts
1223
2158
  function collectPinnedColumns(writePolicy) {
1224
2159
  const key = (pin) => `${pin.column}\0${pin.claim}`;
1225
- const visit = (node) => {
2160
+ const visit2 = (node) => {
1226
2161
  if (node.kind === "and") {
1227
2162
  const merged = [];
1228
2163
  const seen2 = /* @__PURE__ */ new Set();
1229
2164
  for (const operand of node.operands) {
1230
- for (const pin of visit(operand)) {
2165
+ for (const pin of visit2(operand)) {
1231
2166
  if (seen2.has(key(pin))) continue;
1232
2167
  seen2.add(key(pin));
1233
2168
  merged.push(pin);
@@ -1238,9 +2173,9 @@ function collectPinnedColumns(writePolicy) {
1238
2173
  if (node.kind === "or") {
1239
2174
  const [first, ...rest] = node.operands;
1240
2175
  if (first === void 0) return [];
1241
- let common = visit(first);
2176
+ let common = visit2(first);
1242
2177
  for (const operand of rest) {
1243
- const branch = new Set(visit(operand).map(key));
2178
+ const branch = new Set(visit2(operand).map(key));
1244
2179
  common = common.filter((pin) => branch.has(key(pin)));
1245
2180
  if (common.length === 0) break;
1246
2181
  }
@@ -1252,7 +2187,7 @@ function collectPinnedColumns(writePolicy) {
1252
2187
  return [];
1253
2188
  };
1254
2189
  const seen = /* @__PURE__ */ new Set();
1255
- return visit(writePolicy).filter((pin) => {
2190
+ return visit2(writePolicy).filter((pin) => {
1256
2191
  if (seen.has(pin.column)) return false;
1257
2192
  seen.add(pin.column);
1258
2193
  return true;
@@ -1346,6 +2281,11 @@ var PolicyInjector = class {
1346
2281
  where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
1347
2282
  ...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
1348
2283
  };
2284
+ case "aggregateGroups":
2285
+ return {
2286
+ ...root,
2287
+ where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
2288
+ };
1349
2289
  case "mutation":
1350
2290
  return this.injectMutation(root);
1351
2291
  }
@@ -1353,6 +2293,7 @@ var PolicyInjector = class {
1353
2293
  injectMutation(root) {
1354
2294
  const table = root.table;
1355
2295
  const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
2296
+ if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
1356
2297
  if (root.op === "insert" || root.op === "insertOne") {
1357
2298
  const insertPolicy = this.insert(table);
1358
2299
  if (predContainsExists(insertPolicy)) {
@@ -1394,6 +2335,24 @@ var PolicyInjector = class {
1394
2335
  }
1395
2336
  return { ...root, where, returning };
1396
2337
  }
2338
+ /**
2339
+ * Resolved writes are policed against the RESOLVED row, not the sparse
2340
+ * manual row: `where` gates the existing row (update/revert/delete) and
2341
+ * `insertCheck` is re-evaluated as a SELECT over the view after the write,
2342
+ * so link-traversing (exists) policies are representable here.
2343
+ */
2344
+ injectResolvedWrite(root, returning) {
2345
+ const table = root.table;
2346
+ if (root.resolvedAction === "insert") {
2347
+ const pins = (this.ctx.pins.get(table) ?? []).map(
2348
+ (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
2349
+ );
2350
+ return { ...root, pinnedColumns: pins, insertCheck: this.insert(table), returning };
2351
+ }
2352
+ const updatePolicy = this.update(table);
2353
+ const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
2354
+ return root.resolvedAction === "delete" ? { ...root, where, returning } : { ...root, where, insertCheck: updatePolicy, returning };
2355
+ }
1397
2356
  };
1398
2357
  function collectTableSet(roots, ctx) {
1399
2358
  const tables = /* @__PURE__ */ new Set();
@@ -1436,122 +2395,44 @@ function collectTableSet(roots, ctx) {
1436
2395
  if (aggregate.nodes) visitSelection(aggregate.nodes, target);
1437
2396
  }
1438
2397
  };
1439
- for (const root of roots) {
1440
- tables.add(root.table);
1441
- if (root.kind === "select" || root.kind === "byPk") {
1442
- visitPred(root.where, root.table);
1443
- visitSelection(root.selection, root.table);
1444
- }
1445
- if (root.kind === "aggregate") {
1446
- visitPred(root.where, root.table);
1447
- if (root.nodes) visitSelection(root.nodes, root.table);
1448
- }
1449
- if (root.kind === "mutation") {
1450
- if (root.cowSynced !== void 0) tables.add(root.cowSynced);
1451
- if (root.where) visitPred(root.where, root.table);
1452
- if (root.insertCheck) visitPred(root.insertCheck, root.table);
1453
- if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
1454
- if (root.returning) visitSelection(root.returning, root.table);
1455
- }
1456
- }
1457
- return [...tables].sort();
1458
- }
1459
- function injectPolicies({ roots, ctx }) {
1460
- const injector = new PolicyInjector(ctx);
1461
- const injected = roots.map((root) => injector.injectRoot(root));
1462
- return { roots: injected, tableSet: collectTableSet(injected, ctx) };
1463
- }
1464
-
1465
- // src/sdl/naming.ts
1466
- function camelCase(name) {
1467
- const segments = name.split("_").filter((segment) => segment.length > 0);
1468
- const first = segments[0];
1469
- if (first === void 0) return name;
1470
- const head = first.charAt(0).toLowerCase() + first.slice(1);
1471
- return head + segments.slice(1).map(capitalizeFirst).join("");
1472
- }
1473
- function pascalCase(name) {
1474
- return capitalizeFirst(camelCase(name));
1475
- }
1476
- function capitalizeFirst(segment) {
1477
- return segment.charAt(0).toUpperCase() + segment.slice(1);
1478
- }
1479
- function singularizeWord(word) {
1480
- if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
1481
- for (const suffix of ["sses", "xes", "ches", "shes"]) {
1482
- if (word.endsWith(suffix)) return word.slice(0, -2);
2398
+ for (const root of roots) {
2399
+ tables.add(root.table);
2400
+ switch (root.kind) {
2401
+ case "select":
2402
+ case "byPk":
2403
+ visitPred(root.where, root.table);
2404
+ visitSelection(root.selection, root.table);
2405
+ break;
2406
+ case "aggregate":
2407
+ visitPred(root.where, root.table);
2408
+ if (root.nodes) visitSelection(root.nodes, root.table);
2409
+ break;
2410
+ case "aggregateGroups":
2411
+ visitPred(root.where, root.table);
2412
+ break;
2413
+ case "mutation":
2414
+ if (root.cowSynced !== void 0) tables.add(root.cowSynced);
2415
+ for (const source of ctx.tables[root.table]?.resolved?.sources ?? []) tables.add(source.table);
2416
+ if (root.where) visitPred(root.where, root.table);
2417
+ if (root.insertCheck) visitPred(root.insertCheck, root.table);
2418
+ if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
2419
+ if (root.returning) visitSelection(root.returning, root.table);
2420
+ break;
2421
+ default: {
2422
+ const exhausted = root;
2423
+ throw new DocumentCompileError(
2424
+ `internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
2425
+ "internal"
2426
+ );
2427
+ }
2428
+ }
1483
2429
  }
1484
- if (word.endsWith("ss")) return word;
1485
- if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
1486
- return word;
1487
- }
1488
- function typeName(table) {
1489
- const segments = table.split("_").filter((segment) => segment.length > 0);
1490
- const last = segments[segments.length - 1];
1491
- if (last === void 0) return pascalCase(table);
1492
- return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
1493
- }
1494
- function enumTypeName(name) {
1495
- return pascalCase(name);
1496
- }
1497
- var SCALAR_FOR_COLUMN_TYPE = {
1498
- uuid: "uuid",
1499
- text: "String",
1500
- integer: "Int",
1501
- bigint: "bigint",
1502
- numeric: "numeric",
1503
- double: "Float",
1504
- boolean: "Boolean",
1505
- timestamptz: "timestamptz",
1506
- date: "date",
1507
- jsonb: "jsonb"
1508
- };
1509
- function graphqlScalarFor(type) {
1510
- return SCALAR_FOR_COLUMN_TYPE[type];
1511
- }
1512
- var COLUMN_TYPE_BY_SCALAR = new Map([
1513
- ["ID", "text"],
1514
- ...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
1515
- ]);
1516
- function columnTypeForScalar(scalarName) {
1517
- return COLUMN_TYPE_BY_SCALAR.get(scalarName);
2430
+ return [...tables].sort();
1518
2431
  }
1519
- var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
1520
- var PLATFORM_USER_TYPENAME = "PlatformUser";
1521
- function tableNames(table) {
1522
- const root = camelCase(table);
1523
- const rootPascal = pascalCase(table);
1524
- const type = typeName(table);
1525
- return {
1526
- typename: type,
1527
- selectRoot: root,
1528
- byPkRoot: `${root}ByPk`,
1529
- aggregateRoot: `${root}Aggregate`,
1530
- insertRoot: `insert${rootPascal}`,
1531
- insertOneRoot: `insert${rootPascal}One`,
1532
- updateRoot: `update${rootPascal}`,
1533
- updateByPkRoot: `update${rootPascal}ByPk`,
1534
- deleteRoot: `delete${rootPascal}`,
1535
- deleteByPkRoot: `delete${rootPascal}ByPk`,
1536
- boolExp: `${type}BoolExp`,
1537
- orderBy: `${type}OrderBy`,
1538
- selectColumn: `${type}SelectColumn`,
1539
- insertInput: `${type}InsertInput`,
1540
- setInput: `${type}SetInput`,
1541
- appendInput: `${type}AppendInput`,
1542
- incInput: `${type}IncInput`,
1543
- onConflict: `${type}OnConflict`,
1544
- pkColumnsInput: `${type}PkColumnsInput`,
1545
- constraintEnum: `${type}Constraint`,
1546
- updateColumnEnum: `${type}UpdateColumn`,
1547
- mutationResponse: `${type}MutationResponse`,
1548
- aggregate: `${type}Aggregate`,
1549
- aggregateFields: `${type}AggregateFields`,
1550
- sumFields: `${type}SumFields`,
1551
- avgFields: `${type}AvgFields`,
1552
- aggregateBoolExp: `${type}AggregateBoolExp`,
1553
- aggregateBoolExpCount: `${type}AggregateBoolExpCount`
1554
- };
2432
+ function injectPolicies({ roots, ctx }) {
2433
+ const injector = new PolicyInjector(ctx);
2434
+ const injected = roots.map((root) => injector.injectRoot(root));
2435
+ return { roots: injected, tableSet: collectTableSet(injected, ctx) };
1555
2436
  }
1556
2437
 
1557
2438
  // src/compiler-gql/naming.ts
@@ -1561,7 +2442,12 @@ function typeNameForTable(table) {
1561
2442
  }
1562
2443
  function queryRootNames(table) {
1563
2444
  const names = tableNames(table);
1564
- return { select: names.selectRoot, byPk: names.byPkRoot, aggregate: names.aggregateRoot };
2445
+ return {
2446
+ select: names.selectRoot,
2447
+ byPk: names.byPkRoot,
2448
+ aggregate: names.aggregateRoot,
2449
+ aggregateGroups: names.aggregateGroupsRoot
2450
+ };
1565
2451
  }
1566
2452
  function mutationRootNames(table) {
1567
2453
  const names = tableNames(table);
@@ -1654,7 +2540,15 @@ function resolveColumnEntries(table, enums, diagnostics) {
1654
2540
  type = { enum: enumDef.name, values: [...enumDef.values] };
1655
2541
  }
1656
2542
  }
1657
- return { name: column.name, columnName: column.columnName, type, nullable: column.nullable, deprecated: column.deprecated };
2543
+ return {
2544
+ name: column.name,
2545
+ columnName: column.columnName,
2546
+ type,
2547
+ nullable: column.nullable,
2548
+ deprecated: column.deprecated,
2549
+ onUpdateFn: column.onUpdateFn !== void 0,
2550
+ ...column.cell ? { cell: column.cell } : {}
2551
+ };
1658
2552
  });
1659
2553
  }
1660
2554
  function logicalColumn(entries, ref) {
@@ -1690,8 +2584,8 @@ function buildCompileContext(rawCanonical) {
1690
2584
  const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
1691
2585
  const primaryKey = [];
1692
2586
  for (const ref of pkRefs) {
1693
- const resolved = logicalColumn(entries, ref);
1694
- if (!resolved) {
2587
+ const resolved2 = logicalColumn(entries, ref);
2588
+ if (!resolved2) {
1695
2589
  diagnostics.push({
1696
2590
  severity: "error",
1697
2591
  code: "unknown-pk-column",
@@ -1700,9 +2594,13 @@ function buildCompileContext(rawCanonical) {
1700
2594
  });
1701
2595
  continue;
1702
2596
  }
1703
- primaryKey.push(resolved);
2597
+ primaryKey.push(resolved2);
1704
2598
  }
1705
- const active = entries.filter((c) => !c.deprecated);
2599
+ const resolved = isView ? table.resolved : void 0;
2600
+ const data = entries.filter((c) => c.cell === void 0);
2601
+ const cells = cellMap(entries);
2602
+ const active = data.filter((c) => !c.deprecated);
2603
+ const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
1706
2604
  const constraints = [];
1707
2605
  if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
1708
2606
  for (const column of table.columns) {
@@ -1723,14 +2621,17 @@ function buildCompileContext(rawCanonical) {
1723
2621
  // Synced twins (and views reading them, resolved transitively below)
1724
2622
  // are claims-gated — see tableMetaIrSchema.claimsGated.
1725
2623
  claimsGated: relationIsClaimsGated(table.table),
1726
- columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
2624
+ columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
1727
2625
  primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
1728
2626
  links: [],
1729
- filterable: entries.map((c) => c.name),
1730
- orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
1731
- insertable: isView ? [] : active.map((c) => c.name),
1732
- settable: isView ? [] : active.map((c) => c.name),
2627
+ filterable: data.map((c) => c.name),
2628
+ orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
2629
+ insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
2630
+ settable: resolved ? resolvedWritable(data).filter((name) => !primaryKey.includes(name)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
1733
2631
  appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
2632
+ ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
2633
+ ...cells ? { cells } : {},
2634
+ ...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey[0] }) } : {},
1734
2635
  constraints: isView ? [] : constraints,
1735
2636
  dynamicFilterMaxDepth: 6,
1736
2637
  dynamicFilterMaxNodes: 64
@@ -1751,7 +2652,7 @@ function buildCompileContext(rawCanonical) {
1751
2652
  });
1752
2653
  continue;
1753
2654
  }
1754
- const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
2655
+ const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
1755
2656
  const onEntries = link.kind === "toMany" ? targetEntries : entries;
1756
2657
  const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
1757
2658
  const on = logicalColumn(onEntries, link.on);
@@ -1803,7 +2704,7 @@ function buildCompileContext(rawCanonical) {
1803
2704
  compilePolicyFor("write", writePolicies);
1804
2705
  compilePolicyFor("insert", insertPolicies);
1805
2706
  compilePolicyFor("update", updatePolicies);
1806
- if (isView) {
2707
+ if (isView && table.resolved === void 0) {
1807
2708
  writePolicies.set(table.table, CONST_FALSE);
1808
2709
  insertPolicies.set(table.table, CONST_FALSE);
1809
2710
  updatePolicies.set(table.table, CONST_FALSE);
@@ -1842,18 +2743,23 @@ function buildCompileContext(rawCanonical) {
1842
2743
  const queryRoots = /* @__PURE__ */ new Map();
1843
2744
  const mutationRoots = /* @__PURE__ */ new Map();
1844
2745
  for (const table of relations) {
2746
+ if (table.internal === true) continue;
1845
2747
  const roots = queryRootNames(table.table);
1846
2748
  queryRoots.set(roots.select, { kind: "select", table: table.table });
1847
2749
  if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
1848
2750
  queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
1849
2751
  }
1850
2752
  queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
2753
+ if (table.columns.some((column) => column.type !== "jsonb")) {
2754
+ queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
2755
+ }
1851
2756
  const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
1852
2757
  if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
1853
2758
  for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
1854
2759
  mutationRoots.set(field, { op, table: table.table });
1855
2760
  }
1856
2761
  }
2762
+ if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
1857
2763
  }
1858
2764
  for (const table of relations) {
1859
2765
  for (const mutation of table.cowMutations ?? []) {
@@ -1924,6 +2830,49 @@ function buildCompileContext(rawCanonical) {
1924
2830
  diagnostics
1925
2831
  };
1926
2832
  }
2833
+ function registerResolvedRoots({ table, mutationRoots }) {
2834
+ const names = tableNames(table.table);
2835
+ const canInsert = table.policies.write !== void 0 || table.policies.insert !== void 0;
2836
+ const canUpdate = table.policies.write !== void 0 || table.policies.update !== void 0;
2837
+ const add = (field, action) => mutationRoots.set(field, { op: "resolvedWrite", action, table: table.table });
2838
+ if (canInsert) add(names.insertOneRoot, "insert");
2839
+ if (canUpdate) {
2840
+ add(names.updateByPkRoot, "update");
2841
+ add(names.revertByPkRoot, "revert");
2842
+ if ((table.resolved?.deletedBy.length ?? 0) > 0) add(names.deleteByPkRoot, "delete");
2843
+ }
2844
+ }
2845
+ function cellMap(entries) {
2846
+ const cells = {};
2847
+ for (const entry of entries) {
2848
+ if (entry.cell) cells[entry.cell.of] = { ...cells[entry.cell.of], [entry.cell.part]: entry.name };
2849
+ }
2850
+ return Object.keys(cells).length > 0 ? cells : void 0;
2851
+ }
2852
+ function resolvedWritable(entries) {
2853
+ const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
2854
+ return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
2855
+ }
2856
+ function resolvedMeta({ resolved, pk }) {
2857
+ const manual = resolved.sources.find((source) => source.kind === "manual");
2858
+ return {
2859
+ pk,
2860
+ manual: manual?.name ?? "",
2861
+ deletedBy: [...resolved.deletedBy],
2862
+ ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
2863
+ sources: resolved.sources.map((source) => ({
2864
+ name: source.name,
2865
+ kind: source.kind,
2866
+ table: source.table,
2867
+ columns: [...source.columns],
2868
+ ...source.key !== void 0 ? { key: source.key } : {},
2869
+ ...source.fetch !== void 0 ? { fetch: source.fetch } : {},
2870
+ ...source.function !== void 0 ? { function: source.function } : {},
2871
+ ...source.prefetchDefaults !== void 0 ? { prefetchDefaults: source.prefetchDefaults } : {},
2872
+ ...source.writeback !== void 0 ? { writeback: source.writeback } : {}
2873
+ }))
2874
+ };
2875
+ }
1927
2876
  function flattenSelectionSet(selectionSet, fragments, stack) {
1928
2877
  const selections = [];
1929
2878
  for (const selection of selectionSet.selections) {
@@ -1981,8 +2930,148 @@ function computeOpsHash(opHashes) {
1981
2930
  const digest = crypto.createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
1982
2931
  return `sha256:${digest}`;
1983
2932
  }
2933
+ var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "localOnly"]);
2934
+ var fail = (message, code) => {
2935
+ throw new DocumentCompileError(message, code);
2936
+ };
2937
+ function readArgs(node, name) {
2938
+ return (node.directives ?? []).filter((directive) => directive.name.value === name).map((directive) => {
2939
+ const args = {};
2940
+ for (const arg of directive.arguments ?? []) {
2941
+ const key = arg.name.value;
2942
+ if (key === "source" || key === "maxStaleness") {
2943
+ if (arg.value.kind !== graphql.Kind.STRING) fail(`@${name}(${key}:) must be a string literal`, "invalid-source-directive");
2944
+ args[key] = arg.value.value;
2945
+ } else if (key === "mode" && name === "writeback") {
2946
+ if (arg.value.kind !== graphql.Kind.ENUM || !["SYNC", "ASYNC"].includes(arg.value.value)) {
2947
+ fail("@writeback(mode:) must be SYNC or ASYNC", "invalid-source-directive");
2948
+ }
2949
+ args.mode = arg.value.value === "SYNC" ? "sync" : "async";
2950
+ } else {
2951
+ fail(`@${name} does not take argument '${key}'`, "invalid-source-directive");
2952
+ }
2953
+ }
2954
+ if (!args.source) fail(`@${name} needs a source: argument`, "invalid-source-directive");
2955
+ return args;
2956
+ });
2957
+ }
2958
+ function prefetchSourceFor({ resolved, name, directive, table }) {
2959
+ if (!resolved) return fail(`@${directive} is only valid on resolved tables \u2014 '${table}' is not one`, "invalid-source-directive");
2960
+ const source = resolved.sources.find((entry) => entry.name === name);
2961
+ if (!source || source.kind !== "prefetch") {
2962
+ const options = resolved.sources.filter((entry) => entry.kind === "prefetch").map((entry) => entry.name);
2963
+ return fail(`@${directive}(source: "${name}") on '${table}' must name a prefetch source (${options.join(", ") || "none declared"})`, "invalid-source-directive");
2964
+ }
2965
+ return source;
2966
+ }
2967
+ function applyPrefetchDirectives({ node, root, meta }) {
2968
+ const directives = readArgs(node, "prefetch");
2969
+ for (const name of ["writeback", "localOnly"]) {
2970
+ if (readArgs(node, name).length > 0) fail(`@${name} is only valid on resolved-table mutation roots`, "invalid-source-directive");
2971
+ }
2972
+ if (directives.length === 0) return root;
2973
+ if (root.kind !== "select" && root.kind !== "byPk") {
2974
+ return fail(`@prefetch is only valid on select and byPk roots, not '${root.alias}'`, "invalid-source-directive");
2975
+ }
2976
+ const seen = /* @__PURE__ */ new Set();
2977
+ const plans = directives.map((directive) => {
2978
+ const source = prefetchSourceFor({ resolved: meta.resolved, name: directive.source, directive: "prefetch", table: root.table });
2979
+ if (seen.has(source.name)) fail(`@prefetch(source: "${source.name}") appears twice on '${root.alias}'`, "invalid-source-directive");
2980
+ seen.add(source.name);
2981
+ const maxStalenessMs = directive.maxStaleness === void 0 ? void 0 : parseDurationMs(directive.maxStaleness);
2982
+ if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
2983
+ fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
2984
+ }
2985
+ const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk: meta.resolved.pk, source, alias: root.alias });
2986
+ return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
2987
+ });
2988
+ return { ...root, prefetch: plans };
2989
+ }
2990
+ function selectKey({ where, pk, source, alias }) {
2991
+ const leaves = where.kind === "and" ? where.operands : [where];
2992
+ const pinned = leaves.find((leaf) => leaf.kind === "cmp" && leaf.column === pk && (leaf.op === "eq" || leaf.op === "in"));
2993
+ const shape = source.fetch?.shape ?? "single";
2994
+ if (!pinned || pinned.kind !== "cmp") {
2995
+ return fail(
2996
+ `@prefetch(source: "${source.name}") on '${alias}': the root is not key-addressed \u2014 its where must pin '${pk}' with _eq${shape === "batch" ? " or _in" : ""} (${source.name} supports ${shape === "batch" ? "batch" : "single-key"} fetch)`,
2997
+ "prefetch-shape-mismatch"
2998
+ );
2999
+ }
3000
+ if (pinned.op === "in" && shape !== "batch") {
3001
+ return fail(`@prefetch(source: "${source.name}") on '${alias}': ${source.name} only supports single-key fetch, but the root selects '${pk}' with _in`, "prefetch-shape-mismatch");
3002
+ }
3003
+ return pinned.op === "eq" ? { one: pinned.value } : { many: pinned.value };
3004
+ }
3005
+ function applyWritebackDirectives({ node, root, meta }) {
3006
+ if (readArgs(node, "prefetch").length > 0) fail("@prefetch is only valid on query roots", "invalid-source-directive");
3007
+ const writebacks = readArgs(node, "writeback");
3008
+ const localOnly = readArgs(node, "localOnly");
3009
+ if (root.kind !== "mutation" || root.op !== "resolvedWrite" || !meta.resolved) {
3010
+ if (writebacks.length + localOnly.length > 0) fail("@writeback/@localOnly are only valid on resolved-table mutation roots", "invalid-source-directive");
3011
+ return root;
3012
+ }
3013
+ const resolved = meta.resolved;
3014
+ const declared = /* @__PURE__ */ new Map();
3015
+ const bindings = [];
3016
+ for (const directive of writebacks) {
3017
+ const source = prefetchSourceFor({ resolved, name: directive.source, directive: "writeback", table: root.table });
3018
+ if (!source.writeback) fail(`@writeback(source: "${source.name}"): source '${source.name}' declares no writeback columns`, "invalid-source-directive");
3019
+ if (root.resolvedAction !== "update") {
3020
+ fail(`@writeback on '${root.alias}': only update roots write back in v1 (use @localOnly for ${root.resolvedAction ?? "this"} roots)`, "writeback-shape-mismatch");
3021
+ }
3022
+ if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
3023
+ declared.set(source.name, "writeback");
3024
+ bindings.push({ source: source.name, mode: directive.mode ?? source.writeback.defaultMode });
3025
+ }
3026
+ for (const directive of localOnly) {
3027
+ const source = prefetchSourceFor({ resolved, name: directive.source, directive: "localOnly", table: root.table });
3028
+ if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
3029
+ declared.set(source.name, "localOnly");
3030
+ }
3031
+ assertWritebacksDeclared({ root, meta, resolved, declared });
3032
+ return {
3033
+ ...root,
3034
+ ...bindings.length > 0 ? { writeback: bindings } : {},
3035
+ ...localOnly.length > 0 ? { localOnly: localOnly.map((directive) => directive.source) } : {}
3036
+ };
3037
+ }
3038
+ function assertWritebacksDeclared({ root, meta, resolved, declared }) {
3039
+ const written = writtenColumns({ root, meta });
3040
+ for (const source of resolved.sources) {
3041
+ const columns = source.writeback?.columns.filter((column) => written.has(column)) ?? [];
3042
+ if (columns.length > 0 && !declared.has(source.name)) {
3043
+ fail(
3044
+ `'${root.alias}' sets ${columns.map((column) => `'${column}'`).join(", ")}, which source '${source.name}' writes back \u2014 add @writeback(source: "${source.name}") to push the change or @localOnly(source: "${source.name}") to keep it local`,
3045
+ "writeback-undeclared"
3046
+ );
3047
+ }
3048
+ }
3049
+ }
3050
+ function writtenColumns({ root, meta }) {
3051
+ if (root.resolvedAction === "update") {
3052
+ if (!root.set || "slot" in root.set) return new Set(meta.settable);
3053
+ return new Set(root.set.entries.map((entry) => entry.column));
3054
+ }
3055
+ if (root.resolvedAction === "insert") {
3056
+ if (!root.objects || "slot" in root.objects) return new Set(meta.insertable);
3057
+ return new Set(root.objects.rows.flatMap((row) => row.map((entry) => entry.column)));
3058
+ }
3059
+ return /* @__PURE__ */ new Set();
3060
+ }
3061
+
3062
+ // src/compiler-gql/lower.ts
1984
3063
  var DEFAULT_LIMIT = 100;
1985
3064
  var MAX_LIMIT = 1e3;
3065
+ var MAX_GROUPS = 1e3;
3066
+ var DATE_BUCKET_BY_ENUM = {
3067
+ YEAR: "year",
3068
+ QUARTER: "quarter",
3069
+ MONTH: "month",
3070
+ WEEK: "week",
3071
+ DAY: "day",
3072
+ HOUR: "hour"
3073
+ };
3074
+ var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
1986
3075
  var BOOLEXP_OPS = {
1987
3076
  _eq: "eq",
1988
3077
  _neq: "neq",
@@ -2008,16 +3097,24 @@ var ORDER_DIRECTIONS = {
2008
3097
  DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
2009
3098
  };
2010
3099
  var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
2011
- function fail(message, code, candidates = []) {
3100
+ function fail2(message, code, candidates = []) {
2012
3101
  throw new DocumentCompileError(message, code, candidates);
2013
3102
  }
2014
3103
  function argsByName(field) {
2015
3104
  return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
2016
3105
  }
3106
+ function* aggregateBlockFields(selectionSet) {
3107
+ for (const node of selectionSet.selections) {
3108
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3109
+ if (node.name.value === "__typename") continue;
3110
+ if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
3111
+ yield node;
3112
+ }
3113
+ }
2017
3114
  function assertKnownArgs(field, allowed) {
2018
3115
  for (const arg of field.arguments ?? []) {
2019
3116
  if (!allowed.includes(arg.name.value)) {
2020
- fail(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
3117
+ fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
2021
3118
  }
2022
3119
  }
2023
3120
  }
@@ -2030,8 +3127,11 @@ var OperationLowerer = class {
2030
3127
  meta(table) {
2031
3128
  return this.ctx.tables[table];
2032
3129
  }
3130
+ /** A data column — cell-metadata columns are reachable only through `cells { … }`. */
2033
3131
  column(table, name) {
2034
- return this.meta(table).columns.find((c) => c.name === name);
3132
+ const meta = this.meta(table);
3133
+ const column = meta.columns.find((c) => c.name === name);
3134
+ return column && !isCellColumn(meta, name) ? column : void 0;
2035
3135
  }
2036
3136
  link(table, name) {
2037
3137
  return this.meta(table).links.find((l) => l.name === name);
@@ -2039,7 +3139,8 @@ var OperationLowerer = class {
2039
3139
  fieldCandidates(table) {
2040
3140
  const meta = this.meta(table);
2041
3141
  return [
2042
- ...meta.columns.map((c) => c.name),
3142
+ ...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
3143
+ ...meta.cells ? [CELLS_FIELD] : [],
2043
3144
  ...meta.links.map((l) => l.name),
2044
3145
  ...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
2045
3146
  ];
@@ -2048,7 +3149,7 @@ var OperationLowerer = class {
2048
3149
  slot(name, input, table) {
2049
3150
  const variable = this.variables.get(name);
2050
3151
  if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
2051
- fail(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
3152
+ fail2(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
2052
3153
  }
2053
3154
  return name;
2054
3155
  }
@@ -2073,14 +3174,14 @@ var OperationLowerer = class {
2073
3174
  const items = node.values.map((item) => {
2074
3175
  const lowered = this.lowerValue(item);
2075
3176
  if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
2076
- fail("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
3177
+ fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
2077
3178
  }
2078
3179
  return lowered.value;
2079
3180
  });
2080
3181
  return { kind: "literal", value: items };
2081
3182
  }
2082
3183
  case graphql.Kind.OBJECT:
2083
- fail("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
3184
+ fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
2084
3185
  }
2085
3186
  }
2086
3187
  // -------------------------------------------------------------------------
@@ -2090,7 +3191,7 @@ var OperationLowerer = class {
2090
3191
  if (node.kind === graphql.Kind.VARIABLE) {
2091
3192
  return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
2092
3193
  }
2093
- if (node.kind !== graphql.Kind.OBJECT) fail(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
3194
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
2094
3195
  const operands = [];
2095
3196
  for (const field of node.fields) {
2096
3197
  const key = field.name.value;
@@ -2121,45 +3222,45 @@ var OperationLowerer = class {
2121
3222
  continue;
2122
3223
  }
2123
3224
  }
2124
- fail(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
3225
+ fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
2125
3226
  }
2126
3227
  if (operands.length === 0) return CONST_TRUE;
2127
3228
  return operands.length === 1 ? operands[0] : { kind: "and", operands };
2128
3229
  }
2129
3230
  lowerComparison(node, column, table) {
2130
3231
  if (node.kind !== graphql.Kind.OBJECT) {
2131
- fail(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
3232
+ fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
2132
3233
  }
2133
3234
  return node.fields.map((field) => {
2134
3235
  const op = BOOLEXP_OPS[field.name.value];
2135
3236
  if (!op) {
2136
- fail(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
3237
+ fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
2137
3238
  }
2138
3239
  return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
2139
3240
  });
2140
3241
  }
2141
3242
  /** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
2142
3243
  lowerAggPredicate(node, link, target) {
2143
- if (node.kind !== graphql.Kind.OBJECT) fail(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
3244
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
2144
3245
  const countField = node.fields.find((f) => f.name.value === "count");
2145
3246
  if (!countField || node.fields.length !== 1) {
2146
- fail(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
3247
+ fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
2147
3248
  }
2148
- if (countField.value.kind !== graphql.Kind.OBJECT) fail(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
3249
+ if (countField.value.kind !== graphql.Kind.OBJECT) fail2(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
2149
3250
  let predicate;
2150
3251
  let where;
2151
3252
  for (const field of countField.value.fields) {
2152
3253
  if (field.name.value === "predicate") {
2153
3254
  if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
2154
- fail(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
3255
+ fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
2155
3256
  }
2156
3257
  const opField = field.value.fields[0];
2157
3258
  const opName = opField.name.value;
2158
3259
  if (!AGG_PREDICATE_OPS.includes(opName)) {
2159
- fail(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
3260
+ fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
2160
3261
  }
2161
3262
  if (opField.value.kind !== graphql.Kind.INT) {
2162
- fail(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
3263
+ fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
2163
3264
  }
2164
3265
  predicate = { op: opName.slice(1), value: Number(opField.value.value) };
2165
3266
  continue;
@@ -2168,9 +3269,9 @@ var OperationLowerer = class {
2168
3269
  where = this.lowerBoolExp(field.value, target);
2169
3270
  continue;
2170
3271
  }
2171
- fail(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
3272
+ fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
2172
3273
  }
2173
- if (!predicate) fail(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
3274
+ if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
2174
3275
  return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
2175
3276
  }
2176
3277
  lowerOrderBy(node, table) {
@@ -2180,7 +3281,7 @@ var OperationLowerer = class {
2180
3281
  const items = node.kind === graphql.Kind.LIST ? node.values : [node];
2181
3282
  const lowered = [];
2182
3283
  for (const item of items) {
2183
- if (item.kind !== graphql.Kind.OBJECT) fail(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
3284
+ if (item.kind !== graphql.Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
2184
3285
  for (const field of item.fields) {
2185
3286
  const key = field.name.value;
2186
3287
  const column = this.column(table, key);
@@ -2191,25 +3292,25 @@ var OperationLowerer = class {
2191
3292
  const link = this.link(table, key);
2192
3293
  if (link && link.kind !== "toMany") {
2193
3294
  if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
2194
- fail(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
3295
+ fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
2195
3296
  }
2196
3297
  const inner = field.value.fields[0];
2197
3298
  const targetColumn = this.column(link.target, inner.name.value);
2198
3299
  if (!targetColumn) {
2199
- fail(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
3300
+ fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
2200
3301
  }
2201
3302
  lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
2202
3303
  continue;
2203
3304
  }
2204
- fail(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
3305
+ fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
2205
3306
  }
2206
3307
  }
2207
3308
  return lowered;
2208
3309
  }
2209
3310
  lowerDirection(node, table, column) {
2210
- if (node.kind !== graphql.Kind.ENUM) fail(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
3311
+ if (node.kind !== graphql.Kind.ENUM) fail2(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
2211
3312
  const direction = ORDER_DIRECTIONS[node.value];
2212
- if (!direction) fail(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
3313
+ if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
2213
3314
  return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
2214
3315
  }
2215
3316
  lowerDistinctOn(node, table) {
@@ -2218,9 +3319,9 @@ var OperationLowerer = class {
2218
3319
  }
2219
3320
  const items = node.kind === graphql.Kind.LIST ? node.values : [node];
2220
3321
  return items.map((item) => {
2221
- if (item.kind !== graphql.Kind.ENUM) fail(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
3322
+ if (item.kind !== graphql.Kind.ENUM) fail2(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
2222
3323
  if (!this.column(table, item.value)) {
2223
- fail(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3324
+ fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
2224
3325
  }
2225
3326
  return item.value;
2226
3327
  });
@@ -2231,16 +3332,16 @@ var OperationLowerer = class {
2231
3332
  if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
2232
3333
  if (node.kind === graphql.Kind.INT) {
2233
3334
  const value = Number(node.value);
2234
- if (value < 1) fail("limit must be a positive integer", "invalid-limit");
3335
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
2235
3336
  return { ...base, value: Math.min(value, MAX_LIMIT) };
2236
3337
  }
2237
- fail("limit must be an integer literal or a variable", "invalid-limit");
3338
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
2238
3339
  }
2239
3340
  lowerOffset(node) {
2240
3341
  if (!node) return void 0;
2241
3342
  if (node.kind === graphql.Kind.VARIABLE) return { variable: node.name.value };
2242
3343
  if (node.kind === graphql.Kind.INT) return { value: Number(node.value) };
2243
- fail("offset must be an integer literal or a variable", "invalid-offset");
3344
+ fail2("offset must be an integer literal or a variable", "invalid-offset");
2244
3345
  }
2245
3346
  // -------------------------------------------------------------------------
2246
3347
  // Selections
@@ -2249,9 +3350,9 @@ var OperationLowerer = class {
2249
3350
  const meta = this.meta(table);
2250
3351
  const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
2251
3352
  for (const node of selectionSet.selections) {
2252
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3353
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
2253
3354
  if (node.directives && node.directives.length > 0) {
2254
- fail(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
3355
+ fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
2255
3356
  }
2256
3357
  const name = node.name.value;
2257
3358
  if (name === "__typename") continue;
@@ -2259,14 +3360,19 @@ var OperationLowerer = class {
2259
3360
  const column = this.column(table, name);
2260
3361
  if (column) {
2261
3362
  if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
2262
- fail(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
3363
+ fail2(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
2263
3364
  }
2264
3365
  selection.columns.push({ alias, column: name });
2265
3366
  continue;
2266
3367
  }
3368
+ if (name === CELLS_FIELD && meta.cells) {
3369
+ if (selection.cells) fail2(`'${CELLS_FIELD}' may be selected once per object`, "invalid-selection");
3370
+ selection.cells = this.lowerCells(node, table, alias);
3371
+ continue;
3372
+ }
2267
3373
  const link = this.link(table, name);
2268
3374
  if (link) {
2269
- if (!node.selectionSet) fail(`link '${table}.${name}' needs a selection set`, "invalid-selection");
3375
+ if (!node.selectionSet) fail2(`link '${table}.${name}' needs a selection set`, "invalid-selection");
2270
3376
  const relation = {
2271
3377
  alias,
2272
3378
  link: name,
@@ -2285,7 +3391,7 @@ var OperationLowerer = class {
2285
3391
  if (offset) relation.offset = offset;
2286
3392
  }
2287
3393
  } else if (node.arguments && node.arguments.length > 0) {
2288
- fail(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
3394
+ fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
2289
3395
  }
2290
3396
  selection.relations.push(relation);
2291
3397
  continue;
@@ -2293,7 +3399,7 @@ var OperationLowerer = class {
2293
3399
  if (name.endsWith("Aggregate")) {
2294
3400
  const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
2295
3401
  if (aggLink && aggLink.kind === "toMany") {
2296
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3402
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2297
3403
  assertKnownArgs(node, ["where"]);
2298
3404
  const whereArg = argsByName(node).get("where");
2299
3405
  const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
@@ -2306,70 +3412,182 @@ var OperationLowerer = class {
2306
3412
  continue;
2307
3413
  }
2308
3414
  }
2309
- fail(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
3415
+ fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
2310
3416
  }
2311
3417
  return selection;
2312
3418
  }
3419
+ /**
3420
+ * `cells { <column> { at by … } }` — typed per-cell metadata. Plain tables
3421
+ * offer `at`/`by`; resolved tables add the winning `source` and newer
3422
+ * `conflicts { source at value }`.
3423
+ */
3424
+ lowerCells(node, table, alias) {
3425
+ const meta = this.meta(table);
3426
+ const cells = meta.cells ?? {};
3427
+ if (node.arguments && node.arguments.length > 0) fail2(`'${CELLS_FIELD}' takes no arguments`, "invalid-selection");
3428
+ if (!node.selectionSet) fail2(`'${CELLS_FIELD}' needs a selection set`, "invalid-selection");
3429
+ const fields = [];
3430
+ for (const cellNode of fieldNodes(node.selectionSet)) {
3431
+ const column = cellNode.name.value;
3432
+ const parts = cells[column];
3433
+ if (!parts || parts.at === void 0 && parts.source === void 0) {
3434
+ fail2(`unknown cell '${column}' on '${table}'`, "unknown-field", Object.keys(cells));
3435
+ }
3436
+ if (!cellNode.selectionSet) fail2(`cell '${column}' needs a selection set`, "invalid-selection");
3437
+ const resolved = parts.source !== void 0;
3438
+ const allowed = resolved ? ["source", "at", "by", "conflicts"] : ["at", "by"];
3439
+ const selected = [];
3440
+ for (const partNode of fieldNodes(cellNode.selectionSet)) {
3441
+ const part = partNode.name.value;
3442
+ if (!allowed.includes(part)) fail2(`unknown field '${part}' on cell '${column}'`, "unknown-field", allowed);
3443
+ const partAlias = partNode.alias?.value ?? part;
3444
+ if (part !== "conflicts") {
3445
+ if (partNode.selectionSet) fail2(`cell field '${part}' takes no sub-selection`, "invalid-selection");
3446
+ selected.push({ alias: partAlias, part });
3447
+ continue;
3448
+ }
3449
+ if (!partNode.selectionSet) fail2(`'conflicts' needs a selection set`, "invalid-selection");
3450
+ const conflictFields = fieldNodes(partNode.selectionSet).map((fieldNode) => {
3451
+ const field = fieldNode.name.value;
3452
+ if (field !== "source" && field !== "at" && field !== "value") fail2(`unknown field '${field}' on a cell conflict`, "unknown-field", ["source", "at", "value"]);
3453
+ return { alias: fieldNode.alias?.value ?? field, field };
3454
+ });
3455
+ selected.push({ alias: partAlias, part: "conflicts", fields: conflictFields, typename: CELL_TYPENAMES.conflict });
3456
+ }
3457
+ fields.push({
3458
+ alias: cellNode.alias?.value ?? column,
3459
+ column,
3460
+ typename: resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp,
3461
+ parts: selected
3462
+ });
3463
+ }
3464
+ return { alias, typename: cellsTypename(meta.typename), fields };
3465
+ }
2313
3466
  /** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
2314
3467
  lowerAggregateSelection(selectionSet, table) {
2315
3468
  const result = {};
2316
3469
  let sawAggregate = false;
2317
- for (const node of selectionSet.selections) {
2318
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3470
+ for (const node of aggregateBlockFields(selectionSet)) {
2319
3471
  const name = node.name.value;
2320
- if (name === "__typename") continue;
2321
- if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
2322
3472
  if (name === "nodes") {
2323
- if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
3473
+ if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
2324
3474
  result.nodes = this.lowerSelection(node.selectionSet, table);
2325
3475
  continue;
2326
3476
  }
2327
- if (name !== "aggregate") fail(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
2328
- if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3477
+ if (name !== "aggregate") fail2(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
3478
+ if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
2329
3479
  sawAggregate = true;
2330
- if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
2331
- for (const aggField of node.selectionSet.selections) {
2332
- if (aggField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
2333
- const aggName = aggField.name.value;
2334
- if (aggName === "__typename") continue;
2335
- if (aggField.alias) fail(`aliases are not supported inside aggregate blocks ('${aggField.alias.value}')`, "invalid-aggregate");
2336
- if (aggName === "count") {
2337
- assertKnownArgs(aggField, ["columns", "distinct"]);
2338
- const args = argsByName(aggField);
2339
- const count = {};
2340
- const columns = args.get("columns");
2341
- if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
2342
- const distinct = args.get("distinct");
2343
- if (distinct) {
2344
- if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
2345
- count.distinct = distinct.value.value;
2346
- }
2347
- result.count = count;
2348
- continue;
3480
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
3481
+ Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
3482
+ }
3483
+ if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
3484
+ return result;
3485
+ }
3486
+ /**
3487
+ * The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
3488
+ * `aggregate` sub-block as the ungrouped shape (shared via
3489
+ * `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
3490
+ * `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
3491
+ */
3492
+ lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
3493
+ const groupColumns = new Set(groupBy.map((entry) => entry.column));
3494
+ const result = { key: [] };
3495
+ let sawAggregate = false;
3496
+ let sawKey = false;
3497
+ for (const node of aggregateBlockFields(selectionSet)) {
3498
+ const name = node.name.value;
3499
+ if (name === "key") {
3500
+ if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
3501
+ sawKey = true;
3502
+ if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
3503
+ result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
3504
+ continue;
3505
+ }
3506
+ if (name === "nodes") {
3507
+ fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
3508
+ }
3509
+ if (name !== "aggregate") fail2(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
3510
+ if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3511
+ sawAggregate = true;
3512
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
3513
+ Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
3514
+ }
3515
+ const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
3516
+ if (result.key.length === 0 && !rendersAggregate) {
3517
+ fail2(
3518
+ "grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
3519
+ "invalid-aggregate",
3520
+ ["key", "aggregate"]
3521
+ );
3522
+ }
3523
+ return result;
3524
+ }
3525
+ /**
3526
+ * `key { month officer }` → the projected group columns. Every field must be
3527
+ * one of the query's own group keys: any other column is indeterminate
3528
+ * within a group, so selecting it is a document error rather than an
3529
+ * arbitrary pick.
3530
+ */
3531
+ lowerGroupKeySelection(selectionSet, table, groupColumns) {
3532
+ const key = [];
3533
+ for (const node of selectionSet.selections) {
3534
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3535
+ const name = node.name.value;
3536
+ if (name === "__typename") continue;
3537
+ if (!this.column(table, name)) {
3538
+ fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
3539
+ }
3540
+ if (!groupColumns.has(name)) {
3541
+ fail2(
3542
+ `key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
3543
+ "invalid-group-key",
3544
+ [...groupColumns]
3545
+ );
3546
+ }
3547
+ key.push({ alias: node.alias?.value ?? name, column: name });
3548
+ }
3549
+ return key;
3550
+ }
3551
+ /** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
3552
+ lowerAggregateFields(selectionSet, table) {
3553
+ const result = {};
3554
+ for (const aggField of aggregateBlockFields(selectionSet)) {
3555
+ const aggName = aggField.name.value;
3556
+ if (aggName === "count") {
3557
+ assertKnownArgs(aggField, ["columns", "distinct"]);
3558
+ const args = argsByName(aggField);
3559
+ const count = {};
3560
+ const columns = args.get("columns");
3561
+ if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
3562
+ const distinct = args.get("distinct");
3563
+ if (distinct) {
3564
+ if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
3565
+ count.distinct = distinct.value.value;
2349
3566
  }
2350
- if (aggName === "sum" || aggName === "avg") {
2351
- if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
2352
- const columns = [];
2353
- for (const columnField of aggField.selectionSet.selections) {
2354
- if (columnField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
2355
- if (columnField.name.value === "__typename") continue;
2356
- const column = this.column(table, columnField.name.value);
2357
- if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
2358
- fail(
2359
- `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
2360
- "invalid-aggregate",
2361
- this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
2362
- );
2363
- }
2364
- columns.push(columnField.name.value);
3567
+ result.count = count;
3568
+ continue;
3569
+ }
3570
+ if (aggName === "sum" || aggName === "avg") {
3571
+ if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
3572
+ const columns = [];
3573
+ for (const columnField of aggField.selectionSet.selections) {
3574
+ if (columnField.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3575
+ if (columnField.name.value === "__typename") continue;
3576
+ const column = this.column(table, columnField.name.value);
3577
+ if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
3578
+ fail2(
3579
+ `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
3580
+ "invalid-aggregate",
3581
+ this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
3582
+ );
2365
3583
  }
2366
- result[aggName] = columns;
2367
- continue;
3584
+ columns.push(columnField.name.value);
2368
3585
  }
2369
- fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
3586
+ result[aggName] = columns;
3587
+ continue;
2370
3588
  }
3589
+ fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
2371
3590
  }
2372
- if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
2373
3591
  return result;
2374
3592
  }
2375
3593
  // -------------------------------------------------------------------------
@@ -2378,32 +3596,65 @@ var OperationLowerer = class {
2378
3596
  lowerRoots(operation, kind) {
2379
3597
  const roots = [];
2380
3598
  for (const node of operation.selectionSet.selections) {
2381
- if (node.kind !== graphql.Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
3599
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
3600
+ if (node.selectionSet) graphql.visit(node.selectionSet, {
3601
+ Directive(directive) {
3602
+ if (directive.name.value === "assertAffectedRows") {
3603
+ fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
3604
+ }
3605
+ }
3606
+ });
3607
+ const assertion = this.parseAffectedRowAssertion(node, kind);
2382
3608
  const gate = this.parseRootDirectives(node);
2383
3609
  if (gate.omit) continue;
2384
- const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
2385
- roots.push(gate.includeIf ? { ...root, includeIf: gate.includeIf } : root);
3610
+ const lowered = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
3611
+ const meta = this.meta(lowered.table);
3612
+ const root = kind === "query" ? applyPrefetchDirectives({ node, root: lowered, meta }) : applyWritebackDirectives({ node, root: lowered, meta });
3613
+ const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
3614
+ roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
2386
3615
  }
2387
- if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
3616
+ if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
2388
3617
  return roots;
2389
3618
  }
3619
+ parseAffectedRowAssertion(node, kind) {
3620
+ const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
3621
+ if (!assertions.length) return void 0;
3622
+ if (kind !== "mutation" || assertions.length !== 1) {
3623
+ fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
3624
+ }
3625
+ if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
3626
+ fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
3627
+ }
3628
+ const args = assertions[0].arguments;
3629
+ const arg = args?.[0];
3630
+ if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
3631
+ fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
3632
+ }
3633
+ const count = Number(arg.value.value);
3634
+ if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
3635
+ fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
3636
+ }
3637
+ return count;
3638
+ }
2390
3639
  parseRootDirectives(node) {
2391
3640
  let includeIf;
2392
3641
  let omit = false;
2393
3642
  for (const directive of node.directives ?? []) {
2394
3643
  const name = directive.name.value;
3644
+ if (name === "assertAffectedRows") continue;
3645
+ if (SOURCE_DIRECTIVES.has(name)) continue;
2395
3646
  if (name !== "include" && name !== "skip") {
2396
- fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
3647
+ fail2(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
2397
3648
  }
2398
3649
  const ifArg = directive.arguments?.find((a) => a.name.value === "if");
2399
- if (!ifArg) fail(`@${name} needs an if: argument`, "invalid-directive");
3650
+ if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
2400
3651
  if (ifArg.value.kind === graphql.Kind.BOOLEAN) {
2401
3652
  const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
2402
3653
  if (!keep) omit = true;
2403
3654
  continue;
2404
3655
  }
2405
- if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
2406
- if (includeIf) fail("at most one of @include/@skip per root field", "invalid-directive");
3656
+ if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail2(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
3657
+ if (includeIf) fail2("at most one of @include/@skip per root field", "invalid-directive");
2407
3658
  includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
2408
3659
  }
2409
3660
  return omit ? { omit: true } : { includeIf, omit: false };
@@ -2412,12 +3663,12 @@ var OperationLowerer = class {
2412
3663
  const name = node.name.value;
2413
3664
  const alias = node.alias?.value ?? name;
2414
3665
  const rootInfo = this.ctx.queryRoots.get(name);
2415
- if (!rootInfo) fail(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
3666
+ if (!rootInfo) fail2(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
2416
3667
  const { table } = rootInfo;
2417
3668
  const args = argsByName(node);
2418
3669
  if (rootInfo.kind === "select") {
2419
3670
  assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
2420
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3671
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2421
3672
  const where = args.get("where");
2422
3673
  const orderBy = args.get("orderBy");
2423
3674
  const distinctOn = args.get("distinctOn");
@@ -2436,12 +3687,15 @@ var OperationLowerer = class {
2436
3687
  };
2437
3688
  }
2438
3689
  if (rootInfo.kind === "byPk") {
2439
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3690
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2440
3691
  const pk = this.lowerPkArgs(node, table);
2441
3692
  return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
2442
3693
  }
3694
+ if (rootInfo.kind === "aggregateGroups") {
3695
+ return this.lowerAggregateGroupsRoot(node, { alias, table });
3696
+ }
2443
3697
  assertKnownArgs(node, ["where"]);
2444
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3698
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2445
3699
  const whereArg = args.get("where");
2446
3700
  const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
2447
3701
  return {
@@ -2452,6 +3706,108 @@ var OperationLowerer = class {
2452
3706
  ...aggregate
2453
3707
  };
2454
3708
  }
3709
+ /** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
3710
+ lowerAggregateGroupsRoot(node, { alias, table }) {
3711
+ assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
3712
+ if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
3713
+ const args = argsByName(node);
3714
+ const groupByArg = args.get("groupBy");
3715
+ if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
3716
+ const whereArg = args.get("where");
3717
+ const groupBy = this.lowerGroupBy(groupByArg.value, table);
3718
+ const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
3719
+ const offset = this.lowerOffset(args.get("offset")?.value);
3720
+ return {
3721
+ kind: "aggregateGroups",
3722
+ alias,
3723
+ table,
3724
+ where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
3725
+ groupBy,
3726
+ key: aggregate.key,
3727
+ ...aggregate.count ? { count: aggregate.count } : {},
3728
+ ...aggregate.sum ? { sum: aggregate.sum } : {},
3729
+ ...aggregate.avg ? { avg: aggregate.avg } : {},
3730
+ limit: this.lowerGroupLimit(args.get("limit")?.value),
3731
+ ...offset ? { offset } : {}
3732
+ };
3733
+ }
3734
+ /**
3735
+ * `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
3736
+ *
3737
+ * Document-authored only: a whole-argument variable is rejected rather than
3738
+ * becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
3739
+ * and dynamic-input.ts refuses aggregation shapes from client input on the
3740
+ * same reasoning — `where` stays dynamic, which is what runtime filtering
3741
+ * actually needs.
3742
+ */
3743
+ lowerGroupBy(node, table) {
3744
+ if (node.kind === graphql.Kind.VARIABLE) {
3745
+ fail2(
3746
+ `groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
3747
+ "dynamic-group-by"
3748
+ );
3749
+ }
3750
+ const items = node.kind === graphql.Kind.LIST ? node.values : [node];
3751
+ if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
3752
+ const seen = /* @__PURE__ */ new Set();
3753
+ return items.map((item) => {
3754
+ if (item.kind !== graphql.Kind.OBJECT) {
3755
+ fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
3756
+ }
3757
+ const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
3758
+ for (const key of fields.keys()) {
3759
+ if (key !== "column" && key !== "bucket") {
3760
+ fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
3761
+ }
3762
+ }
3763
+ const columnNode = fields.get("column");
3764
+ if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
3765
+ if (columnNode.kind !== graphql.Kind.ENUM) {
3766
+ fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
3767
+ }
3768
+ const column = this.column(table, columnNode.value);
3769
+ if (!column) {
3770
+ fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3771
+ }
3772
+ if (column.type === "jsonb") {
3773
+ fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
3774
+ }
3775
+ const bucketNode = fields.get("bucket");
3776
+ let bucket;
3777
+ if (bucketNode) {
3778
+ if (bucketNode.kind !== graphql.Kind.ENUM) {
3779
+ fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3780
+ }
3781
+ bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
3782
+ if (!bucket) {
3783
+ fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3784
+ }
3785
+ if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
3786
+ fail2(
3787
+ `groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
3788
+ "invalid-group-by"
3789
+ );
3790
+ }
3791
+ }
3792
+ if (seen.has(columnNode.value)) {
3793
+ fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
3794
+ }
3795
+ seen.add(columnNode.value);
3796
+ return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
3797
+ });
3798
+ }
3799
+ /** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
3800
+ lowerGroupLimit(node) {
3801
+ const base = { default: MAX_GROUPS, max: MAX_GROUPS };
3802
+ if (!node) return base;
3803
+ if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
3804
+ if (node.kind === graphql.Kind.INT) {
3805
+ const value = Number(node.value);
3806
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
3807
+ return { ...base, value: Math.min(value, MAX_GROUPS) };
3808
+ }
3809
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
3810
+ }
2455
3811
  /** byPk-style pk arguments: one argument per primary-key column, exactly. */
2456
3812
  lowerPkArgs(node, table) {
2457
3813
  const meta = this.meta(table);
@@ -2459,7 +3815,7 @@ var OperationLowerer = class {
2459
3815
  assertKnownArgs(node, meta.primaryKey);
2460
3816
  return meta.primaryKey.map((column) => {
2461
3817
  const arg = args.get(column);
2462
- if (!arg) fail(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
3818
+ if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
2463
3819
  return { column, value: this.lowerValue(arg.value) };
2464
3820
  });
2465
3821
  }
@@ -2470,19 +3826,22 @@ var OperationLowerer = class {
2470
3826
  const name = node.name.value;
2471
3827
  const alias = node.alias?.value ?? name;
2472
3828
  const rootInfo = this.ctx.mutationRoots.get(name);
2473
- if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
3829
+ if (!rootInfo) fail2(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
2474
3830
  if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
2475
3831
  return this.lowerCowRoot(node, rootInfo);
2476
3832
  }
3833
+ if (rootInfo.op === "resolvedWrite") {
3834
+ return this.lowerResolvedRoot(node, rootInfo);
3835
+ }
2477
3836
  const { op, table } = rootInfo;
2478
3837
  const args = argsByName(node);
2479
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3838
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2480
3839
  const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
2481
3840
  switch (op) {
2482
3841
  case "insert": {
2483
3842
  assertKnownArgs(node, ["objects", "onConflict"]);
2484
3843
  const objects = args.get("objects");
2485
- if (!objects) fail(`'${name}' needs objects:`, "missing-argument");
3844
+ if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
2486
3845
  const response = this.lowerMutationResponse(node.selectionSet, table);
2487
3846
  return {
2488
3847
  ...base,
@@ -2494,7 +3853,7 @@ var OperationLowerer = class {
2494
3853
  case "insertOne": {
2495
3854
  assertKnownArgs(node, ["object", "onConflict"]);
2496
3855
  const object = args.get("object");
2497
- if (!object) fail(`'${name}' needs object:`, "missing-argument");
3856
+ if (!object) fail2(`'${name}' needs object:`, "missing-argument");
2498
3857
  return {
2499
3858
  ...base,
2500
3859
  objects: this.lowerInsertObjects(object.value, table, { list: false }),
@@ -2505,7 +3864,7 @@ var OperationLowerer = class {
2505
3864
  case "update": {
2506
3865
  assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
2507
3866
  const where = args.get("where");
2508
- if (!where) fail(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
3867
+ if (!where) fail2(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
2509
3868
  return {
2510
3869
  ...base,
2511
3870
  ...this.lowerMutationResponse(node.selectionSet, table),
@@ -2516,7 +3875,7 @@ var OperationLowerer = class {
2516
3875
  case "updateByPk": {
2517
3876
  assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
2518
3877
  const pkColumns = args.get("pkColumns");
2519
- if (!pkColumns) fail(`'${name}' needs pkColumns:`, "missing-argument");
3878
+ if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
2520
3879
  return {
2521
3880
  ...base,
2522
3881
  pk: this.lowerPkColumnsArg(pkColumns.value, table),
@@ -2527,7 +3886,7 @@ var OperationLowerer = class {
2527
3886
  case "delete": {
2528
3887
  assertKnownArgs(node, ["where"]);
2529
3888
  const where = args.get("where");
2530
- if (!where) fail(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
3889
+ if (!where) fail2(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
2531
3890
  return {
2532
3891
  ...base,
2533
3892
  ...this.lowerMutationResponse(node.selectionSet, table),
@@ -2554,11 +3913,11 @@ var OperationLowerer = class {
2554
3913
  const name = node.name.value;
2555
3914
  const alias = node.alias?.value ?? name;
2556
3915
  const { table } = rootInfo;
2557
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3916
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
2558
3917
  const meta = this.meta(table);
2559
3918
  const args = argsByName(node);
2560
3919
  const pkArg = args.get(rootInfo.pk);
2561
- if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
3920
+ if (!pkArg) fail2(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
2562
3921
  const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
2563
3922
  const base = {
2564
3923
  kind: "mutation",
@@ -2579,24 +3938,78 @@ var OperationLowerer = class {
2579
3938
  const entries = rootInfo.set.map((column) => {
2580
3939
  const arg = args.get(column);
2581
3940
  if (!arg) {
2582
- fail(
3941
+ fail2(
2583
3942
  `'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
2584
3943
  "missing-argument",
2585
3944
  [...rootInfo.set]
2586
3945
  );
2587
3946
  }
2588
3947
  if (!meta.settable.includes(column)) {
2589
- fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
3948
+ fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
2590
3949
  }
2591
3950
  return { column, value: this.lowerValue(arg.value) };
2592
3951
  });
2593
3952
  return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
2594
3953
  }
3954
+ /**
3955
+ * Resolved-table roots: insertOne(object), updateByPk(pkColumns, _set),
3956
+ * deleteByPk(pk…), revertByPk(pkColumns, columns). The executor writes only
3957
+ * the touched cells into the manual source; `returning` reads the view.
3958
+ */
3959
+ lowerResolvedRoot(node, rootInfo) {
3960
+ const name = node.name.value;
3961
+ const alias = node.alias?.value ?? name;
3962
+ const { table, action } = rootInfo;
3963
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3964
+ const args = argsByName(node);
3965
+ const base = {
3966
+ kind: "mutation",
3967
+ op: "resolvedWrite",
3968
+ resolvedAction: action,
3969
+ alias,
3970
+ table,
3971
+ pinnedColumns: [],
3972
+ affectedRows: false,
3973
+ returning: this.lowerSelection(node.selectionSet, table)
3974
+ };
3975
+ switch (action) {
3976
+ case "insert": {
3977
+ assertKnownArgs(node, ["object"]);
3978
+ const object = args.get("object");
3979
+ if (!object) fail2(`'${name}' needs object:`, "missing-argument");
3980
+ return { ...base, objects: this.lowerInsertObjects(object.value, table, { list: false }) };
3981
+ }
3982
+ case "update": {
3983
+ assertKnownArgs(node, ["pkColumns", "_set"]);
3984
+ const pkColumns = args.get("pkColumns");
3985
+ if (!pkColumns || !args.get("_set")) fail2(`'${name}' needs pkColumns: and _set:`, "missing-argument");
3986
+ return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), ...this.lowerWriteValues(args, table) };
3987
+ }
3988
+ case "delete":
3989
+ return { ...base, pk: this.lowerPkArgs(node, table) };
3990
+ case "revert": {
3991
+ assertKnownArgs(node, ["pkColumns", "columns"]);
3992
+ const pkColumns = args.get("pkColumns");
3993
+ const columns = args.get("columns");
3994
+ if (!pkColumns || !columns) fail2(`'${name}' needs pkColumns: and columns:`, "missing-argument");
3995
+ const items = columns.value.kind === graphql.Kind.LIST ? columns.value.values : [columns.value];
3996
+ const meta = this.meta(table);
3997
+ const revertColumns = items.map((item) => {
3998
+ if (item.kind !== graphql.Kind.ENUM || !meta.settable.includes(item.value)) {
3999
+ fail2(`'${name}' columns: must list update-column enum values written in the document`, "invalid-revert-columns", meta.settable);
4000
+ }
4001
+ return item.value;
4002
+ });
4003
+ if (revertColumns.length === 0) fail2(`'${name}' columns: must name at least one column`, "invalid-revert-columns");
4004
+ return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), revertColumns: [...new Set(revertColumns)] };
4005
+ }
4006
+ }
4007
+ }
2595
4008
  lowerMutationResponse(selectionSet, table) {
2596
4009
  let affectedRows = false;
2597
4010
  let returning = null;
2598
4011
  for (const node of selectionSet.selections) {
2599
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
4012
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
2600
4013
  const name = node.name.value;
2601
4014
  if (name === "__typename") continue;
2602
4015
  if (name === "affectedRows") {
@@ -2604,34 +4017,34 @@ var OperationLowerer = class {
2604
4017
  continue;
2605
4018
  }
2606
4019
  if (name === "returning") {
2607
- if (!node.selectionSet) fail("'returning' needs a selection set", "invalid-selection");
4020
+ if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
2608
4021
  returning = this.lowerSelection(node.selectionSet, table);
2609
4022
  continue;
2610
4023
  }
2611
- fail(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
4024
+ fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
2612
4025
  }
2613
4026
  return { affectedRows, returning };
2614
4027
  }
2615
4028
  lowerInsertObjects(node, table, { list }) {
2616
4029
  if (node.kind === graphql.Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
2617
4030
  if (list) {
2618
- if (node.kind !== graphql.Kind.LIST) fail(`objects: must be a list literal or a variable`, "invalid-insert");
4031
+ if (node.kind !== graphql.Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
2619
4032
  return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
2620
4033
  }
2621
4034
  return { rows: [this.lowerInsertRow(node, table)] };
2622
4035
  }
2623
4036
  lowerInsertRow(node, table) {
2624
- if (node.kind !== graphql.Kind.OBJECT) fail(`insert rows on '${table}' must be objects`, "invalid-insert");
4037
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
2625
4038
  const meta = this.meta(table);
2626
4039
  return node.fields.map((field) => {
2627
4040
  const key = field.name.value;
2628
4041
  if (this.link(table, key)) {
2629
- fail(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
4042
+ fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
2630
4043
  }
2631
4044
  if (!meta.insertable.includes(key)) {
2632
4045
  const column = this.column(table, key);
2633
- if (column) fail(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
2634
- fail(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
4046
+ if (column) fail2(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
4047
+ fail2(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
2635
4048
  }
2636
4049
  return { column: key, value: this.lowerValue(field.value) };
2637
4050
  });
@@ -2640,32 +4053,32 @@ var OperationLowerer = class {
2640
4053
  if (!arg) return {};
2641
4054
  const node = arg.value;
2642
4055
  if (node.kind === graphql.Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
2643
- if (node.kind !== graphql.Kind.OBJECT) fail("onConflict: must be an object literal or a variable", "invalid-on-conflict");
4056
+ if (node.kind !== graphql.Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
2644
4057
  const meta = this.meta(table);
2645
4058
  let constraint;
2646
4059
  const updateColumns = [];
2647
4060
  for (const field of node.fields) {
2648
4061
  if (field.name.value === "constraint") {
2649
- if (field.value.kind !== graphql.Kind.ENUM) fail("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
4062
+ if (field.value.kind !== graphql.Kind.ENUM) fail2("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
2650
4063
  constraint = field.value.value;
2651
4064
  continue;
2652
4065
  }
2653
4066
  if (field.name.value === "updateColumns") {
2654
4067
  const items = field.value.kind === graphql.Kind.LIST ? field.value.values : [field.value];
2655
4068
  for (const item of items) {
2656
- if (item.kind !== graphql.Kind.ENUM) fail("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
4069
+ if (item.kind !== graphql.Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
2657
4070
  if (!meta.settable.includes(item.value)) {
2658
- fail(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
4071
+ fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
2659
4072
  }
2660
4073
  updateColumns.push(item.value);
2661
4074
  }
2662
4075
  continue;
2663
4076
  }
2664
- fail(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
4077
+ fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
2665
4078
  }
2666
- if (!constraint) fail("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
4079
+ if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
2667
4080
  if (!meta.constraints.some((c) => c.name === constraint)) {
2668
- fail(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
4081
+ fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
2669
4082
  }
2670
4083
  return { onConflict: { constraint, updateColumns } };
2671
4084
  }
@@ -2683,36 +4096,36 @@ var OperationLowerer = class {
2683
4096
  const inc = args.get("_inc");
2684
4097
  if (inc) {
2685
4098
  if (inc.value.kind === graphql.Kind.VARIABLE) {
2686
- fail("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
4099
+ fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
2687
4100
  }
2688
4101
  const numeric = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
2689
4102
  result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric, "_inc") };
2690
4103
  }
2691
4104
  if (!result.set && !result.append && !result.inc) {
2692
- fail(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
4105
+ fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
2693
4106
  }
2694
4107
  return result;
2695
4108
  }
2696
4109
  lowerColumnEntries(node, table, whitelist, argName) {
2697
- if (node.kind !== graphql.Kind.OBJECT) fail(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
4110
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
2698
4111
  return node.fields.map((field) => {
2699
4112
  const key = field.name.value;
2700
4113
  if (!whitelist.includes(key)) {
2701
- fail(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
4114
+ fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
2702
4115
  }
2703
4116
  return { column: key, value: this.lowerValue(field.value) };
2704
4117
  });
2705
4118
  }
2706
4119
  lowerPkColumnsArg(node, table) {
2707
- if (node.kind !== graphql.Kind.OBJECT) fail(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
4120
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
2708
4121
  const meta = this.meta(table);
2709
4122
  const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
2710
4123
  for (const key of provided.keys()) {
2711
- if (!meta.primaryKey.includes(key)) fail(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
4124
+ if (!meta.primaryKey.includes(key)) fail2(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
2712
4125
  }
2713
4126
  return meta.primaryKey.map((column) => {
2714
4127
  const value = provided.get(column);
2715
- if (!value) fail(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
4128
+ if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
2716
4129
  return { column, value: this.lowerValue(value) };
2717
4130
  });
2718
4131
  }
@@ -2721,6 +4134,19 @@ function lowerOperation({ restricted, ctx }) {
2721
4134
  const lowerer = new OperationLowerer(ctx, restricted);
2722
4135
  return lowerer.lowerRoots(restricted.definition, restricted.kind);
2723
4136
  }
4137
+ function isCellColumn(meta, name) {
4138
+ if (!meta.cells) return false;
4139
+ return Object.values(meta.cells).some((parts) => Object.values(parts).includes(name));
4140
+ }
4141
+ function fieldNodes(selectionSet) {
4142
+ const out = [];
4143
+ for (const node of selectionSet.selections) {
4144
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
4145
+ if (node.directives && node.directives.length > 0) fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
4146
+ if (node.name.value !== "__typename") out.push(node);
4147
+ }
4148
+ return out;
4149
+ }
2724
4150
  function parseSdl(sdl) {
2725
4151
  return graphql.buildSchema(sdl);
2726
4152
  }
@@ -2961,7 +4387,7 @@ function compileNamespace({ canonical, sdl, documents }) {
2961
4387
  const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
2962
4388
  const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
2963
4389
  operations[restricted.name] = {
2964
- irVersion: IR_VERSION,
4390
+ irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
2965
4391
  name: restricted.name,
2966
4392
  opHash: computeOpHash({ definition: flattened, schemaHash }),
2967
4393
  kind: restricted.kind,
@@ -2988,7 +4414,7 @@ function compileNamespace({ canonical, sdl, documents }) {
2988
4414
  }
2989
4415
  if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
2990
4416
  const artifact = {
2991
- irVersion: IR_VERSION,
4417
+ irVersion: requiredArtifactIrVersion(Object.values(operations)),
2992
4418
  namespace: ctx.namespace,
2993
4419
  schemaHash,
2994
4420
  opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),