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