@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/cli.cjs CHANGED
@@ -39,9 +39,519 @@ var PredicateParseError = class extends Error {
39
39
  }
40
40
  name = "PredicateParseError";
41
41
  };
42
+
43
+ // src/compiler/sql.ts
44
+ var CLAIMS_SETTING = "request.jwt.claims";
45
+ var CAPABILITIES_CLAIM_KEY = "capabilities";
46
+ var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
47
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
48
+ var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
49
+ var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
50
+ var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
51
+
52
+ // src/checks/compile.ts
53
+ function tokenize(source) {
54
+ const tokens = [];
55
+ for (let position = 0; position < source.length; ) {
56
+ const char = source[position];
57
+ if (/\s/.test(char)) {
58
+ position++;
59
+ continue;
60
+ }
61
+ if (char === "(" || char === ")" || char === ",") {
62
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
63
+ position++;
64
+ continue;
65
+ }
66
+ if (char === "'") {
67
+ const start = position++;
68
+ let value = "";
69
+ let closed = false;
70
+ while (position < source.length) {
71
+ if (source[position] !== "'") {
72
+ value += source[position++];
73
+ continue;
74
+ }
75
+ if (source[position + 1] === "'") {
76
+ value += "'";
77
+ position += 2;
78
+ continue;
79
+ }
80
+ position++;
81
+ closed = true;
82
+ break;
83
+ }
84
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
85
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
86
+ tokens.push({ kind: "string", value, position: start });
87
+ continue;
88
+ }
89
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
90
+ if (operator) {
91
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
92
+ position += operator[0].length;
93
+ continue;
94
+ }
95
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
96
+ if (number) {
97
+ tokens.push({ kind: "number", value: number[0], position });
98
+ position += number[0].length;
99
+ continue;
100
+ }
101
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
102
+ if (identifier3) {
103
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
104
+ position += identifier3[0].length;
105
+ continue;
106
+ }
107
+ throw new Error(`unexpected character '${char}' at position ${position}`);
108
+ }
109
+ tokens.push({ kind: "end", value: "", position: source.length });
110
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
111
+ return tokens;
112
+ }
113
+ var Parser = class {
114
+ constructor(tokens, columns) {
115
+ this.tokens = tokens;
116
+ this.columns = /* @__PURE__ */ new Map();
117
+ for (const column of columns) {
118
+ for (const alias of [column.name, column.columnName]) {
119
+ const previous = this.columns.get(alias);
120
+ if (previous && previous.columnName !== column.columnName) {
121
+ throw new Error(`ambiguous check column '${alias}'`);
122
+ }
123
+ this.columns.set(alias, column);
124
+ }
125
+ }
126
+ }
127
+ index = 0;
128
+ depth = 0;
129
+ columns;
130
+ parse() {
131
+ const expression = this.parseOr();
132
+ this.expect("end", "end of expression");
133
+ return expression;
134
+ }
135
+ parseOr() {
136
+ let left = this.parseAnd();
137
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
138
+ return left;
139
+ }
140
+ parseAnd() {
141
+ let left = this.parseNot();
142
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
143
+ return left;
144
+ }
145
+ parseNot() {
146
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
147
+ return this.parsePrimary();
148
+ }
149
+ parsePrimary() {
150
+ if (this.peek().kind === "lparen") {
151
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
152
+ this.advance();
153
+ const expression = this.parseOr();
154
+ this.expect("rparen", "')'");
155
+ this.depth--;
156
+ return `(${expression})`;
157
+ }
158
+ return this.parseComparison();
159
+ }
160
+ parseComparison() {
161
+ const left = this.parseValue();
162
+ if (this.matchKeyword("is")) {
163
+ const negate = this.matchKeyword("not");
164
+ this.expectKeyword("null");
165
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
166
+ }
167
+ if (this.matchKeyword("between")) {
168
+ const lower = this.parseValue();
169
+ this.expectKeyword("and");
170
+ const upper = this.parseValue();
171
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
172
+ }
173
+ if (this.matchKeyword("in")) {
174
+ this.expect("lparen", "'(' after IN");
175
+ const values = [this.parseValue()];
176
+ while (this.peek().kind === "comma") {
177
+ this.advance();
178
+ values.push(this.parseValue());
179
+ }
180
+ this.expect("rparen", "')' after IN values");
181
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
182
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
183
+ }
184
+ const operator = this.expect("operator", "a comparison operator");
185
+ const right = this.parseValue();
186
+ if (operator.value === "~" || operator.value === "!~") {
187
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
188
+ if (!right.literal) throw new Error("regex pattern must be a literal");
189
+ }
190
+ return `${left.sql} ${operator.value} ${right.sql}`;
191
+ }
192
+ parseValue() {
193
+ const token = this.advance();
194
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
195
+ if (token.kind === "number") {
196
+ const number = Number(token.value);
197
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
198
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
199
+ }
200
+ return { sql: token.value, literal: true };
201
+ }
202
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
203
+ const keyword = token.value.toLowerCase();
204
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
205
+ if (keyword === "btrim") {
206
+ this.expect("lparen", "'(' after btrim");
207
+ const column2 = this.parseColumn();
208
+ this.expect("rparen", "')' after btrim column");
209
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
210
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
211
+ }
212
+ const column = this.columns.get(token.value);
213
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
214
+ return { sql: quoteIdentifier(column.columnName), column };
215
+ }
216
+ parseColumn() {
217
+ const token = this.expect("identifier", "a column name");
218
+ const column = this.columns.get(token.value);
219
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
220
+ return column;
221
+ }
222
+ peek() {
223
+ return this.tokens[this.index];
224
+ }
225
+ advance() {
226
+ const token = this.peek();
227
+ if (token.kind !== "end") this.index++;
228
+ return token;
229
+ }
230
+ matchKeyword(keyword) {
231
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
232
+ this.advance();
233
+ return true;
234
+ }
235
+ expectKeyword(keyword) {
236
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
237
+ }
238
+ expect(kind, description) {
239
+ const token = this.peek();
240
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
241
+ return this.advance();
242
+ }
243
+ };
244
+ function compileCheckExpression(source, columns) {
245
+ return new Parser(tokenize(source), columns).parse();
246
+ }
247
+
248
+ // src/constants.ts
249
+ var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
250
+ var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
251
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
252
+ var MAX_IDENTIFIER_LENGTH = 63;
253
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
254
+ var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
255
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
256
+ var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
257
+ var ORM_SCHEMA_PREFIX = "orm_";
258
+ var CONSUMER_ROLE_SUFFIX = "_consumer";
259
+ var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
260
+ var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
261
+ var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
262
+ var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
263
+ var HASH_DOMAIN = "sequence-orm-definitions\0";
264
+ var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
265
+ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
266
+ var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
267
+ var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
268
+ var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
269
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
270
+ var CELL_PART_SUFFIX = {
271
+ at: "__at",
272
+ by: "__by",
273
+ rev: "__rev",
274
+ source: "__src",
275
+ conflicts: "__lost"
276
+ };
277
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
278
+ var CELLS_FIELD = "cells";
279
+ function cellColumnName(column, part) {
280
+ return `${column}${CELL_PART_SUFFIX[part]}`;
281
+ }
282
+ var MATERIALIZE_ACTOR = "orm:materialize";
283
+ var MATERIALIZE_CLAIMS_JSON = JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
284
+ function materializedRefreshFunctionName(table) {
285
+ return `${table}__refresh`;
286
+ }
287
+ function materializeLockKey(schema, table) {
288
+ return `orm:materialize:${schema}.${table}`;
289
+ }
290
+ var policyVariantSchema = zod.z.object({
291
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
292
+ when: zod.z.string().min(1).max(1e3),
293
+ /** Exactly one of: allow-all bypass, or a row predicate. */
294
+ allow: zod.z.literal(true).optional(),
295
+ policy: zod.z.string().min(1).max(2e3).optional()
296
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
297
+ message: "A variant declares exactly one of allow: true or policy"
298
+ });
299
+ var policyExpressionSchema = zod.z.union([
300
+ zod.z.string().min(1).max(2e3),
301
+ zod.z.object({
302
+ default: zod.z.string().min(1).max(2e3),
303
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
304
+ })
305
+ ]);
306
+ function parseDurationMs(input) {
307
+ if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
308
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
309
+ if (!match) return void 0;
310
+ const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
311
+ const value = Math.round(Number(match[1]) * factor);
312
+ return value > 0 ? value : void 0;
313
+ }
314
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
315
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
316
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
317
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
318
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
319
+ };
320
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
321
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
322
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
323
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
324
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
325
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
326
+ };
327
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
328
+ /* @__PURE__ */ new Set([
329
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
330
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
331
+ ]);
332
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
333
+ var canonicalFunctionRefSchema = zod.z.object({
334
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
335
+ kind: zod.z.enum(["managed", "native"]),
336
+ id: zod.z.string().min(1).max(200)
337
+ }).strict();
338
+ var canonicalFetchShapeSchema = zod.z.union([
339
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
340
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
341
+ ]);
342
+ var canonicalResolvedSourceSchema = zod.z.object({
343
+ name: identifier,
344
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
345
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
346
+ table: identifier,
347
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
348
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
349
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
350
+ updatedAt: zod.z.string().min(1).max(63).optional(),
351
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
352
+ key: zod.z.string().min(1).max(63).optional(),
353
+ fetch: canonicalFetchShapeSchema.optional(),
354
+ function: canonicalFunctionRefSchema.optional(),
355
+ prefetchDefaults: zod.z.object({
356
+ maxStalenessMs: durationMs,
357
+ timeoutMs: durationMs,
358
+ onError: zod.z.enum(["fallback", "fail"])
359
+ }).strict().optional(),
360
+ writeback: zod.z.object({
361
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
362
+ defaultMode: zod.z.enum(["sync", "async"]),
363
+ timeoutMs: durationMs,
364
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
365
+ clearManualOnConfirm: zod.z.boolean()
366
+ }).strict().optional()
367
+ }).strict();
368
+ var canonicalResolveRuleSchema = zod.z.object({
369
+ column: zod.z.string().min(1).max(63),
370
+ comparator: zod.z.enum(COMPARATORS),
371
+ /** Source names, in tie-break order. */
372
+ candidates: zod.z.array(identifier).min(1).max(16),
373
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
374
+ orderBy: zod.z.string().min(1).max(2e3).optional()
375
+ }).strict();
376
+ var canonicalResolvedSchema = zod.z.object({
377
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
378
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
379
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
380
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
381
+ deletedBy: zod.z.array(identifier).max(1).default([]),
382
+ /**
383
+ * Materialized: the view reads an indexed internal table (`table`) that the
384
+ * data plane keeps in step with `compute`, the internal view holding the
385
+ * resolution query. Omitted when the view computes on read.
386
+ */
387
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
388
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
389
+ policies: zod.z.object({
390
+ write: policyExpressionSchema.optional(),
391
+ insert: policyExpressionSchema.optional(),
392
+ update: policyExpressionSchema.optional()
393
+ }).strict().default({})
394
+ }).strict();
395
+ function validateResolvedView({ view, tables, views = [] }) {
396
+ const resolved = view.resolved;
397
+ if (!resolved) return [];
398
+ const errors = [];
399
+ const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
400
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
401
+ if (!pkColumn) {
402
+ fail3("requires exactly one primary-key column");
403
+ return errors;
404
+ }
405
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
406
+ const scope = {
407
+ view,
408
+ resolved,
409
+ pkColumn,
410
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
411
+ sourceByName: /* @__PURE__ */ new Map(),
412
+ fail: fail3
413
+ };
414
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail3("requires exactly one manual source");
415
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
416
+ for (const source of resolved.sources) {
417
+ if (scope.sourceByName.has(source.name)) fail3(`duplicate source '${source.name}'`);
418
+ scope.sourceByName.set(source.name, source);
419
+ const table = tableByName.get(source.table);
420
+ if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
421
+ else validateSource({ scope, source, table });
422
+ }
423
+ validateRules(scope);
424
+ validateViewCells(scope);
425
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
426
+ for (const name of resolved.deletedBy) {
427
+ if (scope.sourceByName.get(name)?.kind !== "manual") fail3(`deletedBy '${name}' must be the manual source`);
428
+ }
429
+ return errors;
430
+ }
431
+ function validateSource({ scope, source, table }) {
432
+ const { fail: fail3 } = scope;
433
+ if (source.kind === "etl" && table.source === void 0) fail3(`etl source '${source.name}' must reference a synced table`);
434
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
435
+ fail3(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
436
+ }
437
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
438
+ if (source.kind === "manual" && !hasRev) fail3(`manual source '${source.name}' must carry '__rev' cell columns`);
439
+ if (source.kind === "prefetch" && hasRev) fail3(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
440
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
441
+ validateSourceColumns({ scope, source, byName });
442
+ if (source.kind === "etl") {
443
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
444
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
445
+ fail3(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
446
+ }
447
+ }
448
+ validatePrefetchFields({ scope, source });
449
+ }
450
+ function validatePrefetchFields({ scope, source }) {
451
+ const { fail: fail3, pkColumn } = scope;
452
+ if (source.kind !== "prefetch") {
453
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
454
+ fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
455
+ }
456
+ return;
457
+ }
458
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
459
+ fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
460
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
461
+ fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
462
+ }
463
+ for (const column of source.writeback?.columns ?? []) {
464
+ if (!source.columns.includes(column)) fail3(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
465
+ }
466
+ }
467
+ function validateSourceColumns({ scope, source, byName }) {
468
+ const { fail: fail3, pkColumn, dataColumns } = scope;
469
+ for (const column of [pkColumn.name, ...source.columns]) {
470
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
471
+ const theirs = byName.get(column);
472
+ if (!own) fail3(`source '${source.name}' supplies unknown column '${column}'`);
473
+ else if (!theirs) fail3(`source '${source.name}' table '${source.table}' has no column '${column}'`);
474
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail3(`column '${column}' has a different type on source '${source.name}'`);
475
+ }
476
+ }
477
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail3 }) {
478
+ const ruled = /* @__PURE__ */ new Set();
479
+ for (const rule of resolved.resolve) {
480
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
481
+ fail3(`resolve rule targets unknown column '${rule.column}'`);
482
+ continue;
483
+ }
484
+ if (ruled.has(rule.column)) fail3(`column '${rule.column}' has more than one resolve rule`);
485
+ ruled.add(rule.column);
486
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
487
+ fail3(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
488
+ }
489
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail3(`column '${rule.column}' repeats a candidate`);
490
+ for (const candidate of rule.candidates) {
491
+ const source = sourceByName.get(candidate);
492
+ if (!source) fail3(`column '${rule.column}' names unknown candidate source '${candidate}'`);
493
+ else if (!source.columns.includes(rule.column)) fail3(`column '${rule.column}' candidate '${candidate}' does not supply it`);
494
+ }
495
+ }
496
+ for (const column of dataColumns.keys()) {
497
+ if (column !== pkColumn.name && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
498
+ }
499
+ }
500
+ var VIEW_CELL_TYPES = {
501
+ source: "text",
502
+ at: "timestamptz",
503
+ by: "text",
504
+ conflicts: "jsonb"
505
+ };
506
+ function validateViewCells({ view, dataColumns, pkColumn, fail: fail3 }) {
507
+ const expected = /* @__PURE__ */ new Map();
508
+ for (const column of dataColumns.values()) {
509
+ if (column.name === pkColumn.name) continue;
510
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
511
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
512
+ }
513
+ }
514
+ for (const column of view.columns) {
515
+ if (!column.cell) continue;
516
+ const want = expected.get(column.name);
517
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
518
+ fail3(`unexpected cell column '${column.name}'`);
519
+ continue;
520
+ }
521
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail3(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
522
+ expected.delete(column.name);
523
+ }
524
+ for (const name of expected.keys()) fail3(`missing cell column '${name}'`);
525
+ }
526
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
527
+ function validateMaterialized({ scope, tableByName, views }) {
528
+ const { view, resolved, fail: fail3 } = scope;
529
+ const { table: tableName, compute: computeName } = resolved.materialized;
530
+ const table = tableByName.get(tableName);
531
+ if (!table || table.internal !== true || table.materializes !== view.view) {
532
+ fail3(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
533
+ } else {
534
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail3(`materialized table '${tableName}' must have exactly the view's columns`);
535
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail3(`materialized table '${tableName}' must share the view's primary key`);
536
+ }
537
+ const compute = views.find((candidate) => candidate.view === computeName);
538
+ if (!compute || compute.internal !== true) fail3(`compute view '${computeName}' must be an internal view`);
539
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail3(`compute view '${computeName}' must have exactly the view's columns`);
540
+ for (const source of resolved.sources) {
541
+ if (source.kind !== "etl") continue;
542
+ const read = tableByName.get(source.table)?.policies?.read;
543
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
544
+ if (!unconditional) {
545
+ fail3(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
546
+ }
547
+ }
548
+ }
549
+
550
+ // src/canonical/v2.ts
42
551
  var CANONICAL_FORMAT = 2;
43
552
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
44
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
553
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
554
+ var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
45
555
  var COLUMN_TYPES = [
46
556
  "uuid",
47
557
  "text",
@@ -56,59 +566,71 @@ var COLUMN_TYPES = [
56
566
  ];
57
567
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
58
568
  var canonicalEnumSchema = zod.z.object({
59
- name: identifier,
569
+ name: identifier2,
60
570
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
61
571
  });
62
572
  var canonicalColumnSchema = zod.z.object({
63
573
  /** Property name in TS (camelCase); columnName is the Postgres name. */
64
574
  name: zod.z.string().min(1).max(63),
65
- columnName: identifier,
66
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
575
+ columnName: columnIdentifier,
576
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
67
577
  nullable: zod.z.boolean().default(true),
68
578
  unique: zod.z.boolean().default(false),
69
579
  /** Literal default, or one of the allowlisted SQL functions. */
70
580
  default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
71
581
  defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
582
+ /**
583
+ * Stamp this column from the database clock on UPDATE (`now()`). The
584
+ * column stays insertable (defaults fill gaps) but is omitted from
585
+ * SetInput / settable so callers cannot backdate it.
586
+ */
587
+ onUpdateFn: zod.z.enum(["now()"]).optional(),
72
588
  description: zod.z.string().max(500).optional(),
73
589
  /** Migration hints — same semantics as v1. */
74
- renamedFrom: identifier.optional(),
590
+ renamedFrom: columnIdentifier.optional(),
75
591
  cast: zod.z.string().max(200).optional(),
76
- deprecated: zod.z.boolean().default(false)
592
+ deprecated: zod.z.boolean().default(false),
593
+ /**
594
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
595
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
596
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
597
+ * ordinary columns so their hashes are unchanged.
598
+ */
599
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
600
+ }).superRefine((column, ctx) => {
601
+ if (column.onUpdateFn === void 0) return;
602
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
603
+ ctx.addIssue({
604
+ code: "custom",
605
+ path: ["onUpdateFn"],
606
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
607
+ });
608
+ }
77
609
  });
78
610
  var canonicalLinkSchema = zod.z.object({
79
611
  name: zod.z.string().min(1).max(63),
80
612
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
81
613
  /** Target table name (namespace-local). Absent for toPlatformUser. */
82
- target: identifier.optional(),
614
+ target: identifier2.optional(),
83
615
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
84
- on: identifier,
616
+ on: columnIdentifier,
85
617
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
86
- references: identifier.optional(),
618
+ references: columnIdentifier.optional(),
87
619
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
88
620
  enforce: zod.z.boolean().default(false),
621
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
622
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
89
623
  description: zod.z.string().max(500).optional()
90
624
  });
91
625
  var canonicalIndexSchema = zod.z.object({
92
- columns: zod.z.array(identifier).min(1),
626
+ columns: zod.z.array(columnIdentifier).min(1),
93
627
  unique: zod.z.boolean().default(false),
94
- name: identifier.optional()
628
+ name: identifier2.optional()
95
629
  });
96
- var policyVariantSchema = zod.z.object({
97
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
98
- when: zod.z.string().min(1).max(1e3),
99
- /** Exactly one of: allow-all bypass, or a row predicate. */
100
- allow: zod.z.literal(true).optional(),
101
- policy: zod.z.string().min(1).max(2e3).optional()
102
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
103
- message: "A variant declares exactly one of allow: true or policy"
104
- });
105
- var policyExpressionSchema = zod.z.union([
106
- zod.z.string().min(1).max(2e3),
107
- zod.z.object({
108
- default: zod.z.string().min(1).max(2e3),
109
- variants: zod.z.array(policyVariantSchema).max(10).default([])
110
- })
111
- ]);
630
+ var canonicalCheckSchema = zod.z.object({
631
+ name: identifier2,
632
+ expression: zod.z.string().min(1).max(2e3)
633
+ }).strict();
112
634
  var canonicalPoliciesSchema = zod.z.object({
113
635
  read: policyExpressionSchema.optional(),
114
636
  write: policyExpressionSchema.optional(),
@@ -296,7 +818,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
296
818
  "width_bucket"
297
819
  ];
298
820
  var canonicalActionSchema = zod.z.object({
299
- name: identifier,
821
+ name: identifier2,
300
822
  requires: zod.z.string().max(1e3).optional(),
301
823
  params: zod.z.record(zod.z.string(), zod.z.object({
302
824
  type: columnTypeSchema,
@@ -308,9 +830,9 @@ var canonicalActionSchema = zod.z.object({
308
830
  });
309
831
  var canonicalCowMutationSchema = zod.z.object({
310
832
  kind: zod.z.enum(["override", "revert"]),
311
- name: identifier,
833
+ name: identifier2,
312
834
  /** Synced table (same namespace) the overlay copies from / falls back to. */
313
- synced: identifier,
835
+ synced: identifier2,
314
836
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
315
837
  pk: zod.z.string().min(1).max(63),
316
838
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -337,17 +859,28 @@ var canonicalCowMutationSchema = zod.z.object({
337
859
  }
338
860
  }
339
861
  });
862
+ var canonicalForeignKeySchema = zod.z.object({
863
+ name: identifier2,
864
+ columns: zod.z.array(columnIdentifier).min(1).max(8),
865
+ target: identifier2,
866
+ references: zod.z.array(columnIdentifier).min(1).max(8),
867
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
868
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
869
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
870
+ }).strict();
340
871
  var canonicalTableSchema = zod.z.object({
341
- table: identifier,
872
+ table: identifier2,
342
873
  description: zod.z.string().max(2e3).optional(),
343
874
  /** Table-level rename hint (new in v2). */
344
- renamedFrom: identifier.optional(),
875
+ renamedFrom: identifier2.optional(),
345
876
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
346
877
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
347
878
  columns: zod.z.array(canonicalColumnSchema).default([]),
348
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
879
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
349
880
  links: zod.z.array(canonicalLinkSchema).default([]),
350
881
  indexes: zod.z.array(canonicalIndexSchema).default([]),
882
+ /** Omitted by old definitions, preserving their existing content hashes. */
883
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
351
884
  policies: canonicalPoliciesSchema.default({}),
352
885
  /**
353
886
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -356,26 +889,64 @@ var canonicalTableSchema = zod.z.object({
356
889
  */
357
890
  actions: zod.z.array(canonicalActionSchema).default([]),
358
891
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
359
- cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
892
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
893
+ /** Omitted by old definitions, preserving their existing content hashes. */
894
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
895
+ /**
896
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
897
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
898
+ * on a resolved table's internal sources), listed explicitly in `columns`
899
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
900
+ * Omitted when off so existing definitions hash unchanged.
901
+ */
902
+ cellMetadata: zod.z.literal(true).optional(),
903
+ /**
904
+ * System-owned backing table of a resolved table (its manual or prefetch
905
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
906
+ * only the data plane writes them. Omitted when false.
907
+ */
908
+ internal: zod.z.literal(true).optional(),
909
+ /**
910
+ * The materialized copy of resolved table `materializes` (internal): the
911
+ * same columns as that view, including its cell parts, maintained by the
912
+ * data plane. Omitted otherwise.
913
+ */
914
+ materializes: identifier2.optional()
360
915
  });
916
+ var MAX_VIEW_QUERY = 1e5;
917
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
361
918
  var canonicalViewSchema = zod.z.object({
362
- view: identifier,
919
+ view: identifier2,
363
920
  description: zod.z.string().max(2e3).optional(),
364
921
  columns: zod.z.array(canonicalColumnSchema).min(1),
365
922
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
366
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
923
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
367
924
  links: zod.z.array(canonicalLinkSchema).default([]),
368
925
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
369
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
926
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
370
927
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
371
928
  }),
372
929
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
373
- dependencies: zod.z.array(identifier).max(500)
930
+ dependencies: zod.z.array(identifier2).max(500),
931
+ /**
932
+ * Present when this view is a resolved table (defineResolvedTable): the
933
+ * multi-source metadata behind its generated query. Omitted otherwise so
934
+ * existing views hash unchanged.
935
+ */
936
+ resolved: canonicalResolvedSchema.optional(),
937
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
938
+ internal: zod.z.literal(true).optional()
374
939
  }).superRefine((view, ctx) => {
940
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
941
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
942
+ }
375
943
  for (const [index3, column] of view.columns.entries()) {
376
944
  if (column.default !== void 0 || column.defaultFn !== void 0) {
377
945
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
378
946
  }
947
+ if (column.onUpdateFn !== void 0) {
948
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare onUpdateFn` });
949
+ }
379
950
  if (column.unique) {
380
951
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
381
952
  }
@@ -383,15 +954,82 @@ var canonicalViewSchema = zod.z.object({
383
954
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
384
955
  }
385
956
  }
957
+ if (view.resolved === void 0 && view.internal !== true) {
958
+ for (const [index3, column] of view.columns.entries()) {
959
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index3], message: `only resolved tables carry cell columns ('${column.name}')` });
960
+ }
961
+ }
386
962
  for (const [index3, link] of view.links.entries()) {
387
963
  if (link.enforce) {
388
964
  ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
389
965
  }
966
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
967
+ ctx.addIssue({ code: "custom", path: ["links", index3], message: `view link '${link.name}' cannot declare referential actions` });
968
+ }
390
969
  }
391
970
  });
971
+ var CELL_PART_TYPES = {
972
+ at: "timestamptz",
973
+ by: "text",
974
+ rev: "uuid",
975
+ source: "text",
976
+ conflicts: "jsonb"
977
+ };
978
+ function trackedCellColumns(table) {
979
+ const pk = new Set(table.primaryKey ?? []);
980
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
981
+ }
982
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
983
+ function validateCellMetadata(table, index3, ctx) {
984
+ const fail3 = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
985
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
986
+ if (table.materializes !== void 0) return;
987
+ if (table.cellMetadata !== true) {
988
+ for (const { position } of cells) fail3(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index3, "columns", position]);
989
+ return;
990
+ }
991
+ if (table.source !== void 0) {
992
+ fail3(`synced table '${table.table}' cannot track cell metadata`);
993
+ return;
994
+ }
995
+ const tracked = trackedCellColumns(table);
996
+ const byName = new Map(tracked.map((column) => [column.name, column]));
997
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
998
+ if (withRev && table.internal !== true) fail3(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
999
+ const seen = /* @__PURE__ */ new Set();
1000
+ for (const { column, position, cell } of cells) {
1001
+ const path = ["tables", index3, "columns", position];
1002
+ const owner = byName.get(cell.of);
1003
+ if (!owner) {
1004
+ fail3(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
1005
+ continue;
1006
+ }
1007
+ if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail3(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
1008
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
1009
+ fail3(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
1010
+ }
1011
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
1012
+ fail3(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
1013
+ }
1014
+ const key = `${cell.of}:${cell.part}`;
1015
+ if (seen.has(key)) fail3(`duplicate cell column '${column.name}'`, path);
1016
+ seen.add(key);
1017
+ }
1018
+ for (const column of tracked) {
1019
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
1020
+ fail3(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
1021
+ }
1022
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
1023
+ fail3(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
1024
+ }
1025
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
1026
+ if (!seen.has(`${column.name}:${part}`)) fail3(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
1027
+ }
1028
+ }
1029
+ }
392
1030
  var canonicalDefinitionSchema = zod.z.object({
393
1031
  definition_format: zod.z.literal(CANONICAL_FORMAT),
394
- namespace: identifier.max(41),
1032
+ namespace: identifier2.max(41),
395
1033
  description: zod.z.string().max(2e3).optional(),
396
1034
  enums: zod.z.array(canonicalEnumSchema).default([]),
397
1035
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
@@ -409,6 +1047,74 @@ var canonicalDefinitionSchema = zod.z.object({
409
1047
  ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
410
1048
  }
411
1049
  seen.add(table.table);
1050
+ validateCellMetadata(table, index3, ctx);
1051
+ if (table.source !== void 0) {
1052
+ for (const [columnIndex, column] of table.columns.entries()) {
1053
+ if (column.onUpdateFn !== void 0) {
1054
+ ctx.addIssue({
1055
+ code: "custom",
1056
+ path: ["tables", index3, "columns", columnIndex],
1057
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
1058
+ });
1059
+ }
1060
+ }
1061
+ }
1062
+ }
1063
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
1064
+ for (const [tableIndex, table] of definition.tables.entries()) {
1065
+ const names = /* @__PURE__ */ new Set([
1066
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
1067
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
1068
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
1069
+ ]);
1070
+ for (const [index3, link] of table.links.entries()) {
1071
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
1072
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "referential actions require an enforced toOne link" });
1073
+ }
1074
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
1075
+ const source = table.columns.find((column) => column.columnName === link.on);
1076
+ if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "SET NULL requires a nullable source column" });
1077
+ }
1078
+ }
1079
+ for (const [index3, check] of (table.checks ?? []).entries()) {
1080
+ const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index3], message });
1081
+ if (table.source) fail3("synced tables cannot declare checks");
1082
+ if (names.has(check.name)) fail3(`duplicate constraint '${check.name}'`);
1083
+ names.add(check.name);
1084
+ try {
1085
+ compileCheckExpression(check.expression, table.columns);
1086
+ } catch (error) {
1087
+ fail3(error instanceof Error ? error.message : String(error));
1088
+ }
1089
+ }
1090
+ for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
1091
+ const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
1092
+ if (names.has(fk.name)) fail3(`duplicate foreign key '${fk.name}'`);
1093
+ names.add(fk.name);
1094
+ const target = fkTables.get(fk.target);
1095
+ if (table.source || !target || target.source) {
1096
+ fail3("foreign keys must connect owned tables in the same namespace");
1097
+ continue;
1098
+ }
1099
+ if (fk.columns.length !== fk.references.length) fail3("foreign key columns and references must have the same number of columns");
1100
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail3("foreign key columns must not repeat");
1101
+ for (const [position, name] of fk.columns.entries()) {
1102
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
1103
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
1104
+ if (!sourceColumn || !targetColumn) fail3("foreign key references an unknown column");
1105
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail3("foreign key column types must match");
1106
+ }
1107
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
1108
+ if (fk.columns.some((name) => table.columns.find((column) => column.columnName === name)?.nullable === false || table.primaryKey?.includes(name))) fail3("SET NULL requires nullable source columns");
1109
+ }
1110
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
1111
+ if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail3("foreign key target columns must form a unique key");
1112
+ }
1113
+ }
1114
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
1115
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
1116
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
1117
+ }
412
1118
  }
413
1119
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
414
1120
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -504,7 +1210,7 @@ var canonicalDefinitionSchema = zod.z.object({
504
1210
  const visiting = /* @__PURE__ */ new Set();
505
1211
  const visited = /* @__PURE__ */ new Set();
506
1212
  let cycleReported = false;
507
- const visit = (name, path) => {
1213
+ const visit2 = (name, path) => {
508
1214
  if (visited.has(name) || cycleReported) return;
509
1215
  if (visiting.has(name)) {
510
1216
  cycleReported = true;
@@ -518,12 +1224,12 @@ var canonicalDefinitionSchema = zod.z.object({
518
1224
  visiting.add(name);
519
1225
  const view = viewByName.get(name);
520
1226
  for (const dependency of view?.dependencies ?? []) {
521
- if (viewByName.has(dependency)) visit(dependency, [...path, name]);
1227
+ if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
522
1228
  }
523
1229
  visiting.delete(name);
524
1230
  visited.add(name);
525
1231
  };
526
- for (const view of views) visit(view.view, []);
1232
+ for (const view of views) visit2(view.view, []);
527
1233
  });
528
1234
  function canonicalRelations(definition) {
529
1235
  const views = (definition.views ?? []).map((view) => ({
@@ -533,8 +1239,12 @@ function canonicalRelations(definition) {
533
1239
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
534
1240
  links: view.links,
535
1241
  indexes: [],
536
- policies: { read: view.policies.read },
537
- actions: []
1242
+ // A resolved table's write policies (evaluated against the resolved row)
1243
+ // ride alongside the view's read policy.
1244
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
1245
+ actions: [],
1246
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
1247
+ ...view.internal ? { internal: true } : {}
538
1248
  }));
539
1249
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
540
1250
  }
@@ -544,12 +1254,14 @@ function normalizeCanonical(definition) {
544
1254
  ...definition,
545
1255
  enums: sortByName(definition.enums),
546
1256
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
547
- const { cowMutations, ...rest } = t2;
1257
+ const { cowMutations, foreignKeys, checks, ...rest } = t2;
548
1258
  return {
549
1259
  ...rest,
550
1260
  links: sortByName(t2.links),
551
1261
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
552
1262
  actions: sortByName(t2.actions),
1263
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1264
+ ...checks?.length ? { checks: sortByName(checks) } : {},
553
1265
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
554
1266
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
555
1267
  };
@@ -564,25 +1276,6 @@ function normalizeCanonical(definition) {
564
1276
  }
565
1277
  return normalized;
566
1278
  }
567
-
568
- // src/constants.ts
569
- var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
570
- var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
571
- var MAX_IDENTIFIER_LENGTH = 63;
572
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
573
- var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
574
- var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
575
- var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
576
- var ORM_SCHEMA_PREFIX = "orm_";
577
- var CONSUMER_ROLE_SUFFIX = "_consumer";
578
- var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
579
- var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
580
- var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
581
- var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
582
- var HASH_DOMAIN = "sequence-orm-definitions\0";
583
- var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
584
- var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
585
- var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
586
1279
  function stableJson(value) {
587
1280
  if (value === null || typeof value !== "object") return JSON.stringify(value);
588
1281
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -855,8 +1548,19 @@ function isSeqNamespaceConfig(value) {
855
1548
  return typeof value === "object" && value !== null && value.kind === NAMESPACE_CONFIG_BRAND;
856
1549
  }
857
1550
  function serializeNamespace(input) {
858
- const { config, tables } = input;
859
- const views = input.views ?? [];
1551
+ const { config } = input;
1552
+ const views = [...input.views ?? []];
1553
+ const tables = [...input.tables];
1554
+ for (const view of views) {
1555
+ for (const internal of view.config.resolved?.internalTables ?? []) {
1556
+ if (!tables.includes(internal)) tables.push(internal);
1557
+ }
1558
+ }
1559
+ for (const view of [...views]) {
1560
+ for (const internal of view.config.resolved?.internalViews ?? []) {
1561
+ if (!views.includes(internal)) views.push(internal);
1562
+ }
1563
+ }
860
1564
  const enums = input.enums ?? [];
861
1565
  const tablesByName = /* @__PURE__ */ new Map();
862
1566
  for (const table of tables) {
@@ -958,6 +1662,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
958
1662
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
959
1663
  if (table.source !== void 0) candidate.source = table.source;
960
1664
  if (table.source !== void 0) {
1665
+ if (table.config.checks?.length) {
1666
+ throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
1667
+ }
961
1668
  if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
962
1669
  throw new OrmDefinitionError(
963
1670
  `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
@@ -995,6 +1702,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
995
1702
  if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
996
1703
  candidate.columns?.push(column);
997
1704
  }
1705
+ if (table.config.internal === true) candidate.internal = true;
1706
+ if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
1707
+ if (table.config.cellMetadata === true && table.source !== void 0) {
1708
+ throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
1709
+ }
998
1710
  if (table.config.primaryKey !== void 0) {
999
1711
  if (columnLevelPk.length > 0) {
1000
1712
  throw new OrmDefinitionError(
@@ -1008,6 +1720,45 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1008
1720
  } else if (columnLevelPk.length > 0) {
1009
1721
  candidate.primaryKey = columnLevelPk;
1010
1722
  }
1723
+ if (table.config.cellMetadata === true) {
1724
+ candidate.cellMetadata = true;
1725
+ const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
1726
+ const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
1727
+ for (const column of tracked) {
1728
+ for (const part of parts) {
1729
+ candidate.columns?.push({
1730
+ name: cellColumnName(column.name, part),
1731
+ columnName: cellColumnName(column.columnName, part),
1732
+ type: CELL_PART_TYPES[part],
1733
+ nullable: true,
1734
+ unique: false,
1735
+ deprecated: false,
1736
+ cell: { of: column.name, part }
1737
+ });
1738
+ }
1739
+ }
1740
+ }
1741
+ if (table.config.foreignKeys?.length) {
1742
+ if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
1743
+ candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
1744
+ const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
1745
+ throw new OrmDefinitionError(message, table.file);
1746
+ } });
1747
+ if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
1748
+ return {
1749
+ name: fk.name,
1750
+ columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
1751
+ target: target.tableName,
1752
+ references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
1753
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {},
1754
+ ...fk.onDelete ? { onDelete: fk.onDelete } : {},
1755
+ ...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
1756
+ };
1757
+ });
1758
+ }
1759
+ if (table.config.checks?.length) {
1760
+ candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
1761
+ }
1011
1762
  for (const index3 of table.config.indexes ?? []) {
1012
1763
  const entry = {
1013
1764
  columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1060,6 +1811,12 @@ function buildCanonicalView({
1060
1811
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1061
1812
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1062
1813
  }
1814
+ if (column.onUpdateFn !== void 0) {
1815
+ throw new OrmDefinitionError(
1816
+ `view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
1817
+ view.file
1818
+ );
1819
+ }
1063
1820
  if (column.unique) {
1064
1821
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1065
1822
  }
@@ -1097,7 +1854,33 @@ function buildCanonicalView({
1097
1854
  columns,
1098
1855
  ...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
1099
1856
  links,
1100
- policies: { read: toCanonicalPolicy(view.config.policies.read) }
1857
+ policies: { read: toCanonicalPolicy(view.config.policies.read) },
1858
+ ...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
1859
+ ...view.config.internal === true ? { internal: true } : {}
1860
+ };
1861
+ }
1862
+ function buildCanonicalResolved(resolved) {
1863
+ const policies = {};
1864
+ if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
1865
+ if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
1866
+ if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
1867
+ return {
1868
+ sources: resolved.sources.map((source) => ({
1869
+ name: source.name,
1870
+ kind: source.kind,
1871
+ table: source.table.tableName,
1872
+ columns: [...source.columns],
1873
+ ...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
1874
+ ...source.key !== void 0 ? { key: source.key } : {},
1875
+ ...source.fetch !== void 0 ? { fetch: source.fetch } : {},
1876
+ ...source.function !== void 0 ? { function: { ...source.function } } : {},
1877
+ ...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
1878
+ ...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
1879
+ })),
1880
+ resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
1881
+ deletedBy: [...resolved.deletedBy],
1882
+ ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
1883
+ policies
1101
1884
  };
1102
1885
  }
1103
1886
  function buildCanonicalLink({
@@ -1108,14 +1891,17 @@ function buildCanonicalLink({
1108
1891
  }) {
1109
1892
  const ownerKind = relationKind(relation);
1110
1893
  const ownerName = relationName(relation);
1111
- const fail2 = (message) => {
1894
+ const fail3 = (message) => {
1112
1895
  throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
1113
1896
  };
1114
1897
  if (link.enforce && link.linkKind !== "toOne") {
1115
- return fail2("sets enforce: true, which is only supported on toOne links");
1898
+ return fail3("sets enforce: true, which is only supported on toOne links");
1899
+ }
1900
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
1901
+ return fail3("sets a referential action without enforce: true");
1116
1902
  }
1117
1903
  if (link.linkKind === "toPlatformUser") {
1118
- if (link.target !== void 0) return fail2("is toPlatformUser and must not declare a target table");
1904
+ if (link.target !== void 0) return fail3("is toPlatformUser and must not declare a target table");
1119
1905
  return {
1120
1906
  name: linkName,
1121
1907
  kind: "toPlatformUser",
@@ -1125,13 +1911,13 @@ function buildCanonicalLink({
1125
1911
  ...link.description !== void 0 ? { description: link.description } : {}
1126
1912
  };
1127
1913
  }
1128
- const target = resolveRelationRef({ ref: link.target, fail: fail2 });
1914
+ const target = resolveRelationRef({ ref: link.target, fail: fail3 });
1129
1915
  const targetName = relationName(target);
1130
1916
  if (!relationsByName.has(targetName)) {
1131
- return fail2(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
1917
+ return fail3(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
1132
1918
  }
1133
1919
  if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
1134
- return fail2("cannot enforce a PostgreSQL foreign key involving a view");
1920
+ return fail3("cannot enforce a PostgreSQL foreign key involving a view");
1135
1921
  }
1136
1922
  const owningSide = link.linkKind === "toOne" ? relation : target;
1137
1923
  const referencedSide = link.linkKind === "toOne" ? target : relation;
@@ -1142,22 +1928,24 @@ function buildCanonicalLink({
1142
1928
  on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1143
1929
  ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
1144
1930
  enforce: link.enforce,
1931
+ ...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
1932
+ ...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
1145
1933
  ...link.description !== void 0 ? { description: link.description } : {}
1146
1934
  };
1147
1935
  }
1148
- function resolveRelationRef({ ref, fail: fail2 }) {
1149
- if (ref === void 0) return fail2("declares no target relation");
1936
+ function resolveRelationRef({ ref, fail: fail3 }) {
1937
+ if (ref === void 0) return fail3("declares no target relation");
1150
1938
  if (typeof ref !== "function") return ref;
1151
1939
  let resolved;
1152
1940
  try {
1153
1941
  resolved = ref();
1154
1942
  } catch (error) {
1155
- return fail2(
1943
+ return fail3(
1156
1944
  `target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
1157
1945
  );
1158
1946
  }
1159
1947
  if (!isSeqTable(resolved) && !isSeqView(resolved)) {
1160
- return fail2("target thunk did not return a relation defined with defineTable/defineView");
1948
+ return fail3("target thunk did not return a relation defined with defineTable/defineView");
1161
1949
  }
1162
1950
  return resolved;
1163
1951
  }
@@ -1224,6 +2012,10 @@ async function loadNamespaceFromDir(dir, options = {}) {
1224
2012
  fsCache: false,
1225
2013
  ...options.ormModulePath !== void 0 ? {
1226
2014
  alias: {
2015
+ // The public /drizzle barrel re-exports this pinned dependency.
2016
+ // Explicitly map it before the root alias: otherwise jiti appends
2017
+ // "/drizzle" to the overridden module's filename.
2018
+ "@sequenceholdings/orm/drizzle": module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))).resolve("drizzle-orm"),
1227
2019
  "@sequenceholdings/orm": options.ormModulePath,
1228
2020
  "@sequenceholdings/orm/authoring": options.ormModulePath
1229
2021
  }
@@ -1437,6 +2229,7 @@ var ORDER_BY_ENUM_VALUES = [
1437
2229
  "DESC_NULLS_LAST"
1438
2230
  ];
1439
2231
  var PLATFORM_USER_TYPENAME = "PlatformUser";
2232
+ var DATE_BUCKET_ENUM = "DateBucket";
1440
2233
  function tableNames(table) {
1441
2234
  const root = camelCase(table);
1442
2235
  const rootPascal = pascalCase(table);
@@ -1452,6 +2245,7 @@ function tableNames(table) {
1452
2245
  updateByPkRoot: `update${rootPascal}ByPk`,
1453
2246
  deleteRoot: `delete${rootPascal}`,
1454
2247
  deleteByPkRoot: `delete${rootPascal}ByPk`,
2248
+ revertByPkRoot: `revert${rootPascal}ByPk`,
1455
2249
  boolExp: `${type}BoolExp`,
1456
2250
  orderBy: `${type}OrderBy`,
1457
2251
  selectColumn: `${type}SelectColumn`,
@@ -1469,9 +2263,25 @@ function tableNames(table) {
1469
2263
  sumFields: `${type}SumFields`,
1470
2264
  avgFields: `${type}AvgFields`,
1471
2265
  aggregateBoolExp: `${type}AggregateBoolExp`,
1472
- aggregateBoolExpCount: `${type}AggregateBoolExpCount`
2266
+ aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
2267
+ aggregateGroupsRoot: `${root}AggregateGroups`,
2268
+ groupByColumnEnum: `${type}GroupByColumn`,
2269
+ groupByInput: `${type}GroupBy`,
2270
+ groupKey: `${type}GroupKey`,
2271
+ aggregateGroup: `${type}AggregateGroup`
1473
2272
  };
1474
2273
  }
2274
+ var CELL_TYPENAMES = {
2275
+ /** A plain table's cell: when and by whom it was last written. */
2276
+ stamp: "CellStamp",
2277
+ /** A resolved table's cell: which source won, when, by whom, and newer losers. */
2278
+ resolved: "ResolvedCell",
2279
+ /** A newer candidate that lost to the column's rule. */
2280
+ conflict: "CellConflict"
2281
+ };
2282
+ function cellsTypename(typename) {
2283
+ return `${typename}Cells`;
2284
+ }
1475
2285
 
1476
2286
  // src/codegen-gql/type-policies.ts
1477
2287
  function generateTypePolicies(canonical) {
@@ -1479,13 +2289,13 @@ function generateTypePolicies(canonical) {
1479
2289
  const entries = [];
1480
2290
  let hasPlatformUser = false;
1481
2291
  for (const table of canonicalRelations(def)) {
1482
- if (table.source !== void 0) continue;
2292
+ if (table.source !== void 0 || table.internal === true) continue;
1483
2293
  if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
1484
2294
  if (!table.primaryKey || table.primaryKey.length === 0) continue;
1485
- const keyFields = table.primaryKey.map((identifier2) => {
1486
- const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
2295
+ const keyFields = table.primaryKey.map((identifier3) => {
2296
+ const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
1487
2297
  if (!column) {
1488
- throw new CodegenGqlError(`Primary key column "${identifier2}" not found on relation "${table.table}".`);
2298
+ throw new CodegenGqlError(`Primary key column "${identifier3}" not found on relation "${table.table}".`);
1489
2299
  }
1490
2300
  return fieldName(column.name);
1491
2301
  });
@@ -1502,7 +2312,100 @@ ${body}
1502
2312
  }
1503
2313
  `;
1504
2314
  }
1505
- var IR_VERSION = 1;
2315
+
2316
+ // src/codegen-gql/source-contracts.ts
2317
+ var TS_TYPE = {
2318
+ uuid: "string",
2319
+ text: "string",
2320
+ integer: "number",
2321
+ bigint: "number | string",
2322
+ numeric: "number | string",
2323
+ double: "number | string",
2324
+ boolean: "boolean",
2325
+ timestamptz: "string",
2326
+ date: "string",
2327
+ jsonb: "unknown"
2328
+ };
2329
+ function tsType(column, enums) {
2330
+ if (typeof column.type === "string") return TS_TYPE[column.type] ?? "unknown";
2331
+ const values = enums.get(column.type.enum) ?? [];
2332
+ return values.length > 0 ? values.map((value) => JSON.stringify(value)).join(" | ") : "string";
2333
+ }
2334
+ function generateSourceContracts(canonical) {
2335
+ const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
2336
+ const enums = new Map(def.enums.map((entry) => [entry.name, entry.values]));
2337
+ const blocks = [];
2338
+ for (const view of def.views ?? []) {
2339
+ for (const source of view.resolved?.sources ?? []) {
2340
+ if (source.kind !== "prefetch") continue;
2341
+ const name = `${pascalCase(view.view)}${pascalCase(source.name)}`;
2342
+ const byName = new Map(view.columns.map((column) => [column.name, column]));
2343
+ const key = byName.get(source.key ?? "");
2344
+ const valueLines = source.columns.map((column) => {
2345
+ const def2 = byName.get(column);
2346
+ return ` ${column}?: ${def2 ? tsType(def2, enums) : "unknown"} | null`;
2347
+ });
2348
+ const writable = source.writeback?.columns ?? [];
2349
+ blocks.push([
2350
+ `/** ${view.view} \u2190 ${source.name} (${source.function?.kind ?? "managed"} function '${source.function?.id ?? ""}'). */`,
2351
+ `export type ${name}Key = ${key ? tsType(key, enums) : "string"}`,
2352
+ `export interface ${name}Values {
2353
+ ${valueLines.join("\n")}
2354
+ }`,
2355
+ `export interface ${name}PrefetchRequest {
2356
+ action: 'prefetch'
2357
+ namespace: string
2358
+ table: '${view.view}'
2359
+ source: '${source.name}'
2360
+ keys: ${name}Key[]
2361
+ columns: string[]
2362
+ actor: { sub: string }
2363
+ }`,
2364
+ `export interface ${name}PrefetchResponse {
2365
+ rows: Array<{ key: ${name}Key; values: ${name}Values; cells?: Partial<Record<keyof ${name}Values, string>>; sourceUpdatedAt?: string }>
2366
+ missing?: ${name}Key[]
2367
+ }`,
2368
+ ...writable.length > 0 ? [
2369
+ `export type ${name}WritebackColumn = ${writable.map((column) => JSON.stringify(column)).join(" | ")}`,
2370
+ `export interface ${name}WritebackRequest {
2371
+ action: 'writeback'
2372
+ namespace: string
2373
+ table: '${view.view}'
2374
+ source: '${source.name}'
2375
+ key: ${name}Key
2376
+ changes: Partial<Pick<${name}Values, ${name}WritebackColumn>>
2377
+ before: Partial<Pick<${name}Values, ${name}WritebackColumn>>
2378
+ idempotencyKey: string
2379
+ actor: { sub: string }
2380
+ }`,
2381
+ `/** Respond 200 with this; respond 400/409/422 with { detail } when the source system refuses the change. */
2382
+ export interface ${name}WritebackResponse {
2383
+ values?: ${name}Values
2384
+ cells?: Partial<Record<keyof ${name}Values, string>>
2385
+ sourceUpdatedAt?: string
2386
+ }`
2387
+ ] : []
2388
+ ].join("\n\n"));
2389
+ }
2390
+ }
2391
+ if (blocks.length === 0) return null;
2392
+ return `/* Generated by @sequenceholdings/orm \u2014 resolved-table source function contracts. Do not edit. */
2393
+
2394
+ ${blocks.join("\n\n")}
2395
+ `;
2396
+ }
2397
+ var IR_VERSION = 2;
2398
+ var LEGACY_IR_VERSION = 1;
2399
+ var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
2400
+ var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
2401
+ function requiredArtifactIrVersion(operations) {
2402
+ let required = LEGACY_IR_VERSION;
2403
+ for (const operation of operations) {
2404
+ const { irVersion } = operationVersionSchema.parse(operation);
2405
+ if (irVersion > required) required = irVersion;
2406
+ }
2407
+ return required;
2408
+ }
1506
2409
  var CLAIMS_CONTRACT = {
1507
2410
  sub: "string",
1508
2411
  org: "string?",
@@ -1574,6 +2477,21 @@ var selectionIrSchema = zod.z.lazy(
1574
2477
  () => zod.z.object({
1575
2478
  typename: zod.z.string(),
1576
2479
  columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
2480
+ cells: zod.z.object({
2481
+ alias: zod.z.string(),
2482
+ typename: zod.z.string(),
2483
+ fields: zod.z.array(zod.z.object({
2484
+ alias: zod.z.string(),
2485
+ column: zod.z.string(),
2486
+ typename: zod.z.string(),
2487
+ parts: zod.z.array(zod.z.object({
2488
+ alias: zod.z.string(),
2489
+ part: zod.z.enum(["source", "at", "by", "conflicts"]),
2490
+ fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
2491
+ typename: zod.z.string().optional()
2492
+ }))
2493
+ }))
2494
+ }).optional(),
1577
2495
  relations: zod.z.array(zod.z.object({
1578
2496
  alias: zod.z.string(),
1579
2497
  link: zod.z.string(),
@@ -1595,6 +2513,11 @@ var selectionIrSchema = zod.z.lazy(
1595
2513
  })
1596
2514
  );
1597
2515
  var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
2516
+ var prefetchPlanIrSchema = zod.z.object({
2517
+ source: zod.z.string(),
2518
+ maxStalenessMs: zod.z.number().int().positive().optional(),
2519
+ key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
2520
+ });
1598
2521
  var selectRootSchema = zod.z.object({
1599
2522
  kind: zod.z.literal("select"),
1600
2523
  alias: zod.z.string(),
@@ -1606,7 +2529,8 @@ var selectRootSchema = zod.z.object({
1606
2529
  distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
1607
2530
  limit: limitIrSchema,
1608
2531
  offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
1609
- includeIf: includeIfSchema.optional()
2532
+ includeIf: includeIfSchema.optional(),
2533
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
1610
2534
  });
1611
2535
  var byPkRootSchema = zod.z.object({
1612
2536
  kind: zod.z.literal("byPk"),
@@ -1616,7 +2540,8 @@ var byPkRootSchema = zod.z.object({
1616
2540
  pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
1617
2541
  /** Policy predicate — ANDed with the PK match. */
1618
2542
  where: predIrSchema,
1619
- includeIf: includeIfSchema.optional()
2543
+ includeIf: includeIfSchema.optional(),
2544
+ prefetch: zod.z.array(prefetchPlanIrSchema).optional()
1620
2545
  });
1621
2546
  var aggregateRootSchema = zod.z.object({
1622
2547
  kind: zod.z.literal("aggregate"),
@@ -1629,6 +2554,33 @@ var aggregateRootSchema = zod.z.object({
1629
2554
  nodes: selectionIrSchema.optional(),
1630
2555
  includeIf: includeIfSchema.optional()
1631
2556
  });
2557
+ var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
2558
+ var groupByKeyIrSchema = zod.z.object({
2559
+ column: zod.z.string(),
2560
+ bucket: zod.z.enum(DATE_BUCKETS).optional()
2561
+ });
2562
+ var aggregateGroupsRootSchema = zod.z.object({
2563
+ kind: zod.z.literal("aggregateGroups"),
2564
+ alias: zod.z.string(),
2565
+ table: zod.z.string(),
2566
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
2567
+ where: predIrSchema,
2568
+ groupBy: zod.z.array(groupByKeyIrSchema).min(1),
2569
+ /**
2570
+ * The `key { … }` projection: which group columns appear in each entry's key
2571
+ * object, in selection order. A subset of `groupBy` (the compiler rejects a
2572
+ * key field that is not a group key — it would be indeterminate within the
2573
+ * group). Empty when the document selects only `aggregate`.
2574
+ */
2575
+ key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
2576
+ count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
2577
+ sum: zod.z.array(zod.z.string()).optional(),
2578
+ avg: zod.z.array(zod.z.string()).optional(),
2579
+ /** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
2580
+ limit: limitIrSchema,
2581
+ offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
2582
+ includeIf: includeIfSchema.optional()
2583
+ });
1632
2584
  var writeValuesSchema = zod.z.union([
1633
2585
  zod.z.object({ slot: zod.z.string() }),
1634
2586
  zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
@@ -1647,7 +2599,20 @@ var mutationRootSchema = zod.z.object({
1647
2599
  * coalesce view falls back to synced. Both are gated on a matching synced
1648
2600
  * row and carry the write policy in `where` like update/delete.
1649
2601
  */
1650
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
2602
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
2603
+ /**
2604
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
2605
+ * executor gates on the resolved row (write policy in `where`), writes only
2606
+ * the touched cells into the manual source, then re-checks `insertCheck`
2607
+ * against the resolved row. `revert` clears `revertColumns` from the manual
2608
+ * source; `delete` tombstones the row there.
2609
+ */
2610
+ resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
2611
+ revertColumns: zod.z.array(zod.z.string()).optional(),
2612
+ /** `@writeback(source:, mode:)` directives on this root. */
2613
+ writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
2614
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
2615
+ localOnly: zod.z.array(zod.z.string()).optional(),
1651
2616
  alias: zod.z.string(),
1652
2617
  table: zod.z.string(),
1653
2618
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -1674,12 +2639,15 @@ var mutationRootSchema = zod.z.object({
1674
2639
  insertCheck: predIrSchema.optional(),
1675
2640
  returning: selectionIrSchema.nullable(),
1676
2641
  affectedRows: zod.z.boolean().default(false),
2642
+ /** v2: abort the transaction when the actual write count differs, regardless of selection. */
2643
+ assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
1677
2644
  includeIf: includeIfSchema.optional()
1678
2645
  });
1679
2646
  var rootIrSchema = zod.z.discriminatedUnion("kind", [
1680
2647
  selectRootSchema,
1681
2648
  byPkRootSchema,
1682
2649
  aggregateRootSchema,
2650
+ aggregateGroupsRootSchema,
1683
2651
  mutationRootSchema
1684
2652
  ]);
1685
2653
  var INPUT_REF_KINDS = [
@@ -1709,7 +2677,7 @@ var variableIrSchema = zod.z.object({
1709
2677
  defaultValue: variableDefaultIrSchema.optional()
1710
2678
  });
1711
2679
  var compiledOperationSchema = zod.z.object({
1712
- irVersion: zod.z.literal(IR_VERSION),
2680
+ irVersion: irVersionSchema,
1713
2681
  name: zod.z.string(),
1714
2682
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
1715
2683
  kind: zod.z.enum(["query", "mutation"]),
@@ -1721,6 +2689,50 @@ var compiledOperationSchema = zod.z.object({
1721
2689
  /** Every table the op touches — data-token scoping + audit. */
1722
2690
  tableSet: zod.z.array(zod.z.string()).min(1),
1723
2691
  roots: zod.z.array(rootIrSchema).min(1)
2692
+ }).superRefine((operation, ctx) => {
2693
+ for (const [index3, root] of operation.roots.entries()) {
2694
+ if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
2695
+ if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
2696
+ ctx.addIssue({ code: "custom", path: ["roots", index3], message: "affected-row assertions require unconditional mutation roots and IR v2" });
2697
+ }
2698
+ }
2699
+ });
2700
+ var resolvedSourceIrSchema = zod.z.object({
2701
+ name: zod.z.string(),
2702
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
2703
+ /** Logical table key of the backing relation. */
2704
+ table: zod.z.string(),
2705
+ columns: zod.z.array(zod.z.string()),
2706
+ key: zod.z.string().optional(),
2707
+ fetch: zod.z.union([
2708
+ zod.z.object({ shape: zod.z.literal("single") }),
2709
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
2710
+ ]).optional(),
2711
+ function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
2712
+ prefetchDefaults: zod.z.object({
2713
+ maxStalenessMs: zod.z.number().int().positive(),
2714
+ timeoutMs: zod.z.number().int().positive(),
2715
+ onError: zod.z.enum(["fallback", "fail"])
2716
+ }).optional(),
2717
+ writeback: zod.z.object({
2718
+ columns: zod.z.array(zod.z.string()),
2719
+ defaultMode: zod.z.enum(["sync", "async"]),
2720
+ timeoutMs: zod.z.number().int().positive(),
2721
+ clearManualOnConfirm: zod.z.boolean()
2722
+ }).optional()
2723
+ });
2724
+ var resolvedMetaIrSchema = zod.z.object({
2725
+ /** Logical pk column of the resolved view (single-column). */
2726
+ pk: zod.z.string(),
2727
+ /** Source name of the manual source. */
2728
+ manual: zod.z.string(),
2729
+ sources: zod.z.array(resolvedSourceIrSchema),
2730
+ deletedBy: zod.z.array(zod.z.string()),
2731
+ /**
2732
+ * Materialized: the view reads `table`, which the data plane refreshes from
2733
+ * the `compute` view (claims-gated) whenever it writes a source row.
2734
+ */
2735
+ materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
1724
2736
  });
1725
2737
  var tableMetaIrSchema = zod.z.object({
1726
2738
  table: zod.z.string(),
@@ -1757,18 +2769,45 @@ var tableMetaIrSchema = zod.z.object({
1757
2769
  insertable: zod.z.array(zod.z.string()),
1758
2770
  settable: zod.z.array(zod.z.string()),
1759
2771
  appendable: zod.z.array(zod.z.string()),
2772
+ /**
2773
+ * Logical columns stamped with Postgres `now()` on UPDATE (canonical
2774
+ * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
2775
+ */
2776
+ touchOnUpdate: zod.z.array(zod.z.string()).optional(),
2777
+ /**
2778
+ * Per-cell metadata: logical column → the logical names of its cell-part
2779
+ * columns (which are listed in `columns`, but never filterable, orderable or
2780
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
2781
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
2782
+ * the relation tracks no cells.
2783
+ */
2784
+ cells: zod.z.record(zod.z.string(), zod.z.object({
2785
+ at: zod.z.string().optional(),
2786
+ by: zod.z.string().optional(),
2787
+ rev: zod.z.string().optional(),
2788
+ source: zod.z.string().optional(),
2789
+ conflicts: zod.z.string().optional()
2790
+ }).strict()).optional(),
2791
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
2792
+ resolved: resolvedMetaIrSchema.optional(),
1760
2793
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
1761
2794
  /** Depth/node caps for dynamic filter trees on this table. */
1762
2795
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
1763
2796
  dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
1764
2797
  });
1765
2798
  var compiledArtifactSchema = zod.z.object({
1766
- irVersion: zod.z.literal(IR_VERSION),
2799
+ irVersion: irVersionSchema,
1767
2800
  namespace: zod.z.string(),
1768
2801
  schemaHash: zod.z.string(),
1769
2802
  opsHash: zod.z.string(),
1770
2803
  tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
1771
2804
  operations: zod.z.record(zod.z.string(), compiledOperationSchema)
2805
+ }).superRefine((artifact, ctx) => {
2806
+ for (const [name, operation] of Object.entries(artifact.operations)) {
2807
+ if (operation.irVersion > artifact.irVersion) {
2808
+ ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
2809
+ }
2810
+ }
1772
2811
  });
1773
2812
  var IrVersionError = class extends Error {
1774
2813
  constructor(found) {
@@ -1779,19 +2818,19 @@ var IrVersionError = class extends Error {
1779
2818
  function normalizeIr(raw) {
1780
2819
  if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
1781
2820
  const version = raw.irVersion;
1782
- if (version !== IR_VERSION) throw new IrVersionError(version);
2821
+ if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
1783
2822
  return compiledArtifactSchema.parse(raw);
1784
2823
  }
1785
2824
 
1786
2825
  // src/predicate-v2/ast.ts
1787
2826
  var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
1788
- function walkPolicy(node, visit) {
1789
- visit(node);
2827
+ function walkPolicy(node, visit2) {
2828
+ visit2(node);
1790
2829
  if (node.kind === "and" || node.kind === "or") {
1791
- for (const operand of node.operands) walkPolicy(operand, visit);
2830
+ for (const operand of node.operands) walkPolicy(operand, visit2);
1792
2831
  }
1793
- if (node.kind === "not") walkPolicy(node.operand, visit);
1794
- if (node.kind === "exists") walkPolicy(node.where, visit);
2832
+ if (node.kind === "not") walkPolicy(node.operand, visit2);
2833
+ if (node.kind === "exists") walkPolicy(node.where, visit2);
1795
2834
  }
1796
2835
 
1797
2836
  // src/predicate-v2/errors.ts
@@ -1819,7 +2858,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
1819
2858
  var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
1820
2859
  var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
1821
2860
  var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
1822
- function tokenize(source) {
2861
+ function tokenize2(source) {
1823
2862
  const tokens = [];
1824
2863
  let position = 0;
1825
2864
  while (position < source.length) {
@@ -1898,7 +2937,7 @@ function readString(source, start) {
1898
2937
  }
1899
2938
  throw new PolicyParseError("unterminated string literal", start);
1900
2939
  }
1901
- var Parser = class {
2940
+ var Parser2 = class {
1902
2941
  constructor(tokens) {
1903
2942
  this.tokens = tokens;
1904
2943
  }
@@ -2072,7 +3111,7 @@ var Parser = class {
2072
3111
  function parsePolicy(source) {
2073
3112
  const trimmed = source.trim();
2074
3113
  if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
2075
- return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
3114
+ return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
2076
3115
  }
2077
3116
 
2078
3117
  // src/predicate-v2/compile.ts
@@ -2297,21 +3336,21 @@ function andPred(...preds) {
2297
3336
  if (operands.length === 1) return operands[0];
2298
3337
  return { kind: "and", operands };
2299
3338
  }
2300
- function walkPred(pred, visit) {
2301
- visit(pred);
3339
+ function walkPred(pred, visit2) {
3340
+ visit2(pred);
2302
3341
  switch (pred.kind) {
2303
3342
  case "and":
2304
3343
  case "or":
2305
- for (const operand of pred.operands) walkPred(operand, visit);
3344
+ for (const operand of pred.operands) walkPred(operand, visit2);
2306
3345
  return;
2307
3346
  case "not":
2308
- walkPred(pred.operand, visit);
3347
+ walkPred(pred.operand, visit2);
2309
3348
  return;
2310
3349
  case "exists":
2311
- walkPred(pred.where, visit);
3350
+ walkPred(pred.where, visit2);
2312
3351
  return;
2313
3352
  case "aggPredicate":
2314
- if (pred.where) walkPred(pred.where, visit);
3353
+ if (pred.where) walkPred(pred.where, visit2);
2315
3354
  return;
2316
3355
  default:
2317
3356
  return;
@@ -2328,12 +3367,12 @@ function predContainsExists(pred) {
2328
3367
  // src/compiler-gql/inject.ts
2329
3368
  function collectPinnedColumns(writePolicy) {
2330
3369
  const key = (pin) => `${pin.column}\0${pin.claim}`;
2331
- const visit = (node) => {
3370
+ const visit2 = (node) => {
2332
3371
  if (node.kind === "and") {
2333
3372
  const merged = [];
2334
3373
  const seen2 = /* @__PURE__ */ new Set();
2335
3374
  for (const operand of node.operands) {
2336
- for (const pin of visit(operand)) {
3375
+ for (const pin of visit2(operand)) {
2337
3376
  if (seen2.has(key(pin))) continue;
2338
3377
  seen2.add(key(pin));
2339
3378
  merged.push(pin);
@@ -2344,9 +3383,9 @@ function collectPinnedColumns(writePolicy) {
2344
3383
  if (node.kind === "or") {
2345
3384
  const [first, ...rest] = node.operands;
2346
3385
  if (first === void 0) return [];
2347
- let common = visit(first);
3386
+ let common = visit2(first);
2348
3387
  for (const operand of rest) {
2349
- const branch = new Set(visit(operand).map(key));
3388
+ const branch = new Set(visit2(operand).map(key));
2350
3389
  common = common.filter((pin) => branch.has(key(pin)));
2351
3390
  if (common.length === 0) break;
2352
3391
  }
@@ -2358,7 +3397,7 @@ function collectPinnedColumns(writePolicy) {
2358
3397
  return [];
2359
3398
  };
2360
3399
  const seen = /* @__PURE__ */ new Set();
2361
- return visit(writePolicy).filter((pin) => {
3400
+ return visit2(writePolicy).filter((pin) => {
2362
3401
  if (seen.has(pin.column)) return false;
2363
3402
  seen.add(pin.column);
2364
3403
  return true;
@@ -2452,6 +3491,11 @@ var PolicyInjector = class {
2452
3491
  where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
2453
3492
  ...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
2454
3493
  };
3494
+ case "aggregateGroups":
3495
+ return {
3496
+ ...root,
3497
+ where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
3498
+ };
2455
3499
  case "mutation":
2456
3500
  return this.injectMutation(root);
2457
3501
  }
@@ -2459,6 +3503,7 @@ var PolicyInjector = class {
2459
3503
  injectMutation(root) {
2460
3504
  const table = root.table;
2461
3505
  const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
3506
+ if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
2462
3507
  if (root.op === "insert" || root.op === "insertOne") {
2463
3508
  const insertPolicy = this.insert(table);
2464
3509
  if (predContainsExists(insertPolicy)) {
@@ -2500,6 +3545,24 @@ var PolicyInjector = class {
2500
3545
  }
2501
3546
  return { ...root, where, returning };
2502
3547
  }
3548
+ /**
3549
+ * Resolved writes are policed against the RESOLVED row, not the sparse
3550
+ * manual row: `where` gates the existing row (update/revert/delete) and
3551
+ * `insertCheck` is re-evaluated as a SELECT over the view after the write,
3552
+ * so link-traversing (exists) policies are representable here.
3553
+ */
3554
+ injectResolvedWrite(root, returning) {
3555
+ const table = root.table;
3556
+ if (root.resolvedAction === "insert") {
3557
+ const pins = (this.ctx.pins.get(table) ?? []).map(
3558
+ (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
3559
+ );
3560
+ return { ...root, pinnedColumns: pins, insertCheck: this.insert(table), returning };
3561
+ }
3562
+ const updatePolicy = this.update(table);
3563
+ const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
3564
+ return root.resolvedAction === "delete" ? { ...root, where, returning } : { ...root, where, insertCheck: updatePolicy, returning };
3565
+ }
2503
3566
  };
2504
3567
  function collectTableSet(roots, ctx) {
2505
3568
  const tables = /* @__PURE__ */ new Set();
@@ -2544,20 +3607,34 @@ function collectTableSet(roots, ctx) {
2544
3607
  };
2545
3608
  for (const root of roots) {
2546
3609
  tables.add(root.table);
2547
- if (root.kind === "select" || root.kind === "byPk") {
2548
- visitPred(root.where, root.table);
2549
- visitSelection(root.selection, root.table);
2550
- }
2551
- if (root.kind === "aggregate") {
2552
- visitPred(root.where, root.table);
2553
- if (root.nodes) visitSelection(root.nodes, root.table);
2554
- }
2555
- if (root.kind === "mutation") {
2556
- if (root.cowSynced !== void 0) tables.add(root.cowSynced);
2557
- if (root.where) visitPred(root.where, root.table);
2558
- if (root.insertCheck) visitPred(root.insertCheck, root.table);
2559
- if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
2560
- if (root.returning) visitSelection(root.returning, root.table);
3610
+ switch (root.kind) {
3611
+ case "select":
3612
+ case "byPk":
3613
+ visitPred(root.where, root.table);
3614
+ visitSelection(root.selection, root.table);
3615
+ break;
3616
+ case "aggregate":
3617
+ visitPred(root.where, root.table);
3618
+ if (root.nodes) visitSelection(root.nodes, root.table);
3619
+ break;
3620
+ case "aggregateGroups":
3621
+ visitPred(root.where, root.table);
3622
+ break;
3623
+ case "mutation":
3624
+ if (root.cowSynced !== void 0) tables.add(root.cowSynced);
3625
+ for (const source of ctx.tables[root.table]?.resolved?.sources ?? []) tables.add(source.table);
3626
+ if (root.where) visitPred(root.where, root.table);
3627
+ if (root.insertCheck) visitPred(root.insertCheck, root.table);
3628
+ if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
3629
+ if (root.returning) visitSelection(root.returning, root.table);
3630
+ break;
3631
+ default: {
3632
+ const exhausted = root;
3633
+ throw new DocumentCompileError(
3634
+ `internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
3635
+ "internal"
3636
+ );
3637
+ }
2561
3638
  }
2562
3639
  }
2563
3640
  return [...tables].sort();
@@ -2575,7 +3652,12 @@ function typeNameForTable(table) {
2575
3652
  }
2576
3653
  function queryRootNames(table) {
2577
3654
  const names = tableNames(table);
2578
- return { select: names.selectRoot, byPk: names.byPkRoot, aggregate: names.aggregateRoot };
3655
+ return {
3656
+ select: names.selectRoot,
3657
+ byPk: names.byPkRoot,
3658
+ aggregate: names.aggregateRoot,
3659
+ aggregateGroups: names.aggregateGroupsRoot
3660
+ };
2579
3661
  }
2580
3662
  function mutationRootNames(table) {
2581
3663
  const names = tableNames(table);
@@ -2656,7 +3738,15 @@ function resolveColumnEntries(table, enums, diagnostics) {
2656
3738
  type = { enum: enumDef.name, values: [...enumDef.values] };
2657
3739
  }
2658
3740
  }
2659
- return { name: column.name, columnName: column.columnName, type, nullable: column.nullable, deprecated: column.deprecated };
3741
+ return {
3742
+ name: column.name,
3743
+ columnName: column.columnName,
3744
+ type,
3745
+ nullable: column.nullable,
3746
+ deprecated: column.deprecated,
3747
+ onUpdateFn: column.onUpdateFn !== void 0,
3748
+ ...column.cell ? { cell: column.cell } : {}
3749
+ };
2660
3750
  });
2661
3751
  }
2662
3752
  function logicalColumn(entries, ref) {
@@ -2692,8 +3782,8 @@ function buildCompileContext(rawCanonical) {
2692
3782
  const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
2693
3783
  const primaryKey2 = [];
2694
3784
  for (const ref of pkRefs) {
2695
- const resolved = logicalColumn(entries, ref);
2696
- if (!resolved) {
3785
+ const resolved2 = logicalColumn(entries, ref);
3786
+ if (!resolved2) {
2697
3787
  diagnostics.push({
2698
3788
  severity: "error",
2699
3789
  code: "unknown-pk-column",
@@ -2702,9 +3792,13 @@ function buildCompileContext(rawCanonical) {
2702
3792
  });
2703
3793
  continue;
2704
3794
  }
2705
- primaryKey2.push(resolved);
3795
+ primaryKey2.push(resolved2);
2706
3796
  }
2707
- const active = entries.filter((c) => !c.deprecated);
3797
+ const resolved = isView ? table.resolved : void 0;
3798
+ const data = entries.filter((c) => c.cell === void 0);
3799
+ const cells = cellMap(entries);
3800
+ const active = data.filter((c) => !c.deprecated);
3801
+ const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
2708
3802
  const constraints = [];
2709
3803
  if (primaryKey2.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey2] });
2710
3804
  for (const column of table.columns) {
@@ -2725,14 +3819,17 @@ function buildCompileContext(rawCanonical) {
2725
3819
  // Synced twins (and views reading them, resolved transitively below)
2726
3820
  // are claims-gated — see tableMetaIrSchema.claimsGated.
2727
3821
  claimsGated: relationIsClaimsGated(table.table),
2728
- columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
3822
+ columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
2729
3823
  primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
2730
3824
  links: [],
2731
- filterable: entries.map((c) => c.name),
2732
- orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
2733
- insertable: isView ? [] : active.map((c) => c.name),
2734
- settable: isView ? [] : active.map((c) => c.name),
3825
+ filterable: data.map((c) => c.name),
3826
+ orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
3827
+ insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
3828
+ settable: resolved ? resolvedWritable(data).filter((name) => !primaryKey2.includes(name)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
2735
3829
  appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
3830
+ ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
3831
+ ...cells ? { cells } : {},
3832
+ ...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
2736
3833
  constraints: isView ? [] : constraints,
2737
3834
  dynamicFilterMaxDepth: 6,
2738
3835
  dynamicFilterMaxNodes: 64
@@ -2753,7 +3850,7 @@ function buildCompileContext(rawCanonical) {
2753
3850
  });
2754
3851
  continue;
2755
3852
  }
2756
- const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
3853
+ const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
2757
3854
  const onEntries = link.kind === "toMany" ? targetEntries : entries;
2758
3855
  const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
2759
3856
  const on = logicalColumn(onEntries, link.on);
@@ -2805,7 +3902,7 @@ function buildCompileContext(rawCanonical) {
2805
3902
  compilePolicyFor("write", writePolicies);
2806
3903
  compilePolicyFor("insert", insertPolicies);
2807
3904
  compilePolicyFor("update", updatePolicies);
2808
- if (isView) {
3905
+ if (isView && table.resolved === void 0) {
2809
3906
  writePolicies.set(table.table, CONST_FALSE);
2810
3907
  insertPolicies.set(table.table, CONST_FALSE);
2811
3908
  updatePolicies.set(table.table, CONST_FALSE);
@@ -2844,18 +3941,23 @@ function buildCompileContext(rawCanonical) {
2844
3941
  const queryRoots = /* @__PURE__ */ new Map();
2845
3942
  const mutationRoots = /* @__PURE__ */ new Map();
2846
3943
  for (const table of relations) {
3944
+ if (table.internal === true) continue;
2847
3945
  const roots = queryRootNames(table.table);
2848
3946
  queryRoots.set(roots.select, { kind: "select", table: table.table });
2849
3947
  if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
2850
3948
  queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
2851
3949
  }
2852
3950
  queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
3951
+ if (table.columns.some((column) => column.type !== "jsonb")) {
3952
+ queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
3953
+ }
2853
3954
  const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
2854
3955
  if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
2855
3956
  for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
2856
3957
  mutationRoots.set(field, { op, table: table.table });
2857
3958
  }
2858
3959
  }
3960
+ if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
2859
3961
  }
2860
3962
  for (const table of relations) {
2861
3963
  for (const mutation of table.cowMutations ?? []) {
@@ -2926,6 +4028,49 @@ function buildCompileContext(rawCanonical) {
2926
4028
  diagnostics
2927
4029
  };
2928
4030
  }
4031
+ function registerResolvedRoots({ table, mutationRoots }) {
4032
+ const names = tableNames(table.table);
4033
+ const canInsert = table.policies.write !== void 0 || table.policies.insert !== void 0;
4034
+ const canUpdate = table.policies.write !== void 0 || table.policies.update !== void 0;
4035
+ const add = (field, action) => mutationRoots.set(field, { op: "resolvedWrite", action, table: table.table });
4036
+ if (canInsert) add(names.insertOneRoot, "insert");
4037
+ if (canUpdate) {
4038
+ add(names.updateByPkRoot, "update");
4039
+ add(names.revertByPkRoot, "revert");
4040
+ if ((table.resolved?.deletedBy.length ?? 0) > 0) add(names.deleteByPkRoot, "delete");
4041
+ }
4042
+ }
4043
+ function cellMap(entries) {
4044
+ const cells = {};
4045
+ for (const entry of entries) {
4046
+ if (entry.cell) cells[entry.cell.of] = { ...cells[entry.cell.of], [entry.cell.part]: entry.name };
4047
+ }
4048
+ return Object.keys(cells).length > 0 ? cells : void 0;
4049
+ }
4050
+ function resolvedWritable(entries) {
4051
+ const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
4052
+ return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
4053
+ }
4054
+ function resolvedMeta({ resolved, pk }) {
4055
+ const manual = resolved.sources.find((source) => source.kind === "manual");
4056
+ return {
4057
+ pk,
4058
+ manual: manual?.name ?? "",
4059
+ deletedBy: [...resolved.deletedBy],
4060
+ ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
4061
+ sources: resolved.sources.map((source) => ({
4062
+ name: source.name,
4063
+ kind: source.kind,
4064
+ table: source.table,
4065
+ columns: [...source.columns],
4066
+ ...source.key !== void 0 ? { key: source.key } : {},
4067
+ ...source.fetch !== void 0 ? { fetch: source.fetch } : {},
4068
+ ...source.function !== void 0 ? { function: source.function } : {},
4069
+ ...source.prefetchDefaults !== void 0 ? { prefetchDefaults: source.prefetchDefaults } : {},
4070
+ ...source.writeback !== void 0 ? { writeback: source.writeback } : {}
4071
+ }))
4072
+ };
4073
+ }
2929
4074
  function flattenSelectionSet(selectionSet, fragments, stack) {
2930
4075
  const selections = [];
2931
4076
  for (const selection of selectionSet.selections) {
@@ -2983,8 +4128,148 @@ function computeOpsHash(opHashes) {
2983
4128
  const digest = crypto.createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
2984
4129
  return `sha256:${digest}`;
2985
4130
  }
4131
+ var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "localOnly"]);
4132
+ var fail = (message, code) => {
4133
+ throw new DocumentCompileError(message, code);
4134
+ };
4135
+ function readArgs(node, name) {
4136
+ return (node.directives ?? []).filter((directive) => directive.name.value === name).map((directive) => {
4137
+ const args = {};
4138
+ for (const arg of directive.arguments ?? []) {
4139
+ const key = arg.name.value;
4140
+ if (key === "source" || key === "maxStaleness") {
4141
+ if (arg.value.kind !== graphql.Kind.STRING) fail(`@${name}(${key}:) must be a string literal`, "invalid-source-directive");
4142
+ args[key] = arg.value.value;
4143
+ } else if (key === "mode" && name === "writeback") {
4144
+ if (arg.value.kind !== graphql.Kind.ENUM || !["SYNC", "ASYNC"].includes(arg.value.value)) {
4145
+ fail("@writeback(mode:) must be SYNC or ASYNC", "invalid-source-directive");
4146
+ }
4147
+ args.mode = arg.value.value === "SYNC" ? "sync" : "async";
4148
+ } else {
4149
+ fail(`@${name} does not take argument '${key}'`, "invalid-source-directive");
4150
+ }
4151
+ }
4152
+ if (!args.source) fail(`@${name} needs a source: argument`, "invalid-source-directive");
4153
+ return args;
4154
+ });
4155
+ }
4156
+ function prefetchSourceFor({ resolved, name, directive, table }) {
4157
+ if (!resolved) return fail(`@${directive} is only valid on resolved tables \u2014 '${table}' is not one`, "invalid-source-directive");
4158
+ const source = resolved.sources.find((entry) => entry.name === name);
4159
+ if (!source || source.kind !== "prefetch") {
4160
+ const options = resolved.sources.filter((entry) => entry.kind === "prefetch").map((entry) => entry.name);
4161
+ return fail(`@${directive}(source: "${name}") on '${table}' must name a prefetch source (${options.join(", ") || "none declared"})`, "invalid-source-directive");
4162
+ }
4163
+ return source;
4164
+ }
4165
+ function applyPrefetchDirectives({ node, root, meta }) {
4166
+ const directives = readArgs(node, "prefetch");
4167
+ for (const name of ["writeback", "localOnly"]) {
4168
+ if (readArgs(node, name).length > 0) fail(`@${name} is only valid on resolved-table mutation roots`, "invalid-source-directive");
4169
+ }
4170
+ if (directives.length === 0) return root;
4171
+ if (root.kind !== "select" && root.kind !== "byPk") {
4172
+ return fail(`@prefetch is only valid on select and byPk roots, not '${root.alias}'`, "invalid-source-directive");
4173
+ }
4174
+ const seen = /* @__PURE__ */ new Set();
4175
+ const plans = directives.map((directive) => {
4176
+ const source = prefetchSourceFor({ resolved: meta.resolved, name: directive.source, directive: "prefetch", table: root.table });
4177
+ if (seen.has(source.name)) fail(`@prefetch(source: "${source.name}") appears twice on '${root.alias}'`, "invalid-source-directive");
4178
+ seen.add(source.name);
4179
+ const maxStalenessMs = directive.maxStaleness === void 0 ? void 0 : parseDurationMs(directive.maxStaleness);
4180
+ if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
4181
+ fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
4182
+ }
4183
+ const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk: meta.resolved.pk, source, alias: root.alias });
4184
+ return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
4185
+ });
4186
+ return { ...root, prefetch: plans };
4187
+ }
4188
+ function selectKey({ where, pk, source, alias }) {
4189
+ const leaves = where.kind === "and" ? where.operands : [where];
4190
+ const pinned = leaves.find((leaf) => leaf.kind === "cmp" && leaf.column === pk && (leaf.op === "eq" || leaf.op === "in"));
4191
+ const shape = source.fetch?.shape ?? "single";
4192
+ if (!pinned || pinned.kind !== "cmp") {
4193
+ return fail(
4194
+ `@prefetch(source: "${source.name}") on '${alias}': the root is not key-addressed \u2014 its where must pin '${pk}' with _eq${shape === "batch" ? " or _in" : ""} (${source.name} supports ${shape === "batch" ? "batch" : "single-key"} fetch)`,
4195
+ "prefetch-shape-mismatch"
4196
+ );
4197
+ }
4198
+ if (pinned.op === "in" && shape !== "batch") {
4199
+ return fail(`@prefetch(source: "${source.name}") on '${alias}': ${source.name} only supports single-key fetch, but the root selects '${pk}' with _in`, "prefetch-shape-mismatch");
4200
+ }
4201
+ return pinned.op === "eq" ? { one: pinned.value } : { many: pinned.value };
4202
+ }
4203
+ function applyWritebackDirectives({ node, root, meta }) {
4204
+ if (readArgs(node, "prefetch").length > 0) fail("@prefetch is only valid on query roots", "invalid-source-directive");
4205
+ const writebacks = readArgs(node, "writeback");
4206
+ const localOnly = readArgs(node, "localOnly");
4207
+ if (root.kind !== "mutation" || root.op !== "resolvedWrite" || !meta.resolved) {
4208
+ if (writebacks.length + localOnly.length > 0) fail("@writeback/@localOnly are only valid on resolved-table mutation roots", "invalid-source-directive");
4209
+ return root;
4210
+ }
4211
+ const resolved = meta.resolved;
4212
+ const declared = /* @__PURE__ */ new Map();
4213
+ const bindings = [];
4214
+ for (const directive of writebacks) {
4215
+ const source = prefetchSourceFor({ resolved, name: directive.source, directive: "writeback", table: root.table });
4216
+ if (!source.writeback) fail(`@writeback(source: "${source.name}"): source '${source.name}' declares no writeback columns`, "invalid-source-directive");
4217
+ if (root.resolvedAction !== "update") {
4218
+ fail(`@writeback on '${root.alias}': only update roots write back in v1 (use @localOnly for ${root.resolvedAction ?? "this"} roots)`, "writeback-shape-mismatch");
4219
+ }
4220
+ if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
4221
+ declared.set(source.name, "writeback");
4222
+ bindings.push({ source: source.name, mode: directive.mode ?? source.writeback.defaultMode });
4223
+ }
4224
+ for (const directive of localOnly) {
4225
+ const source = prefetchSourceFor({ resolved, name: directive.source, directive: "localOnly", table: root.table });
4226
+ if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
4227
+ declared.set(source.name, "localOnly");
4228
+ }
4229
+ assertWritebacksDeclared({ root, meta, resolved, declared });
4230
+ return {
4231
+ ...root,
4232
+ ...bindings.length > 0 ? { writeback: bindings } : {},
4233
+ ...localOnly.length > 0 ? { localOnly: localOnly.map((directive) => directive.source) } : {}
4234
+ };
4235
+ }
4236
+ function assertWritebacksDeclared({ root, meta, resolved, declared }) {
4237
+ const written = writtenColumns({ root, meta });
4238
+ for (const source of resolved.sources) {
4239
+ const columns = source.writeback?.columns.filter((column) => written.has(column)) ?? [];
4240
+ if (columns.length > 0 && !declared.has(source.name)) {
4241
+ fail(
4242
+ `'${root.alias}' sets ${columns.map((column) => `'${column}'`).join(", ")}, which source '${source.name}' writes back \u2014 add @writeback(source: "${source.name}") to push the change or @localOnly(source: "${source.name}") to keep it local`,
4243
+ "writeback-undeclared"
4244
+ );
4245
+ }
4246
+ }
4247
+ }
4248
+ function writtenColumns({ root, meta }) {
4249
+ if (root.resolvedAction === "update") {
4250
+ if (!root.set || "slot" in root.set) return new Set(meta.settable);
4251
+ return new Set(root.set.entries.map((entry) => entry.column));
4252
+ }
4253
+ if (root.resolvedAction === "insert") {
4254
+ if (!root.objects || "slot" in root.objects) return new Set(meta.insertable);
4255
+ return new Set(root.objects.rows.flatMap((row) => row.map((entry) => entry.column)));
4256
+ }
4257
+ return /* @__PURE__ */ new Set();
4258
+ }
4259
+
4260
+ // src/compiler-gql/lower.ts
2986
4261
  var DEFAULT_LIMIT = 100;
2987
4262
  var MAX_LIMIT = 1e3;
4263
+ var MAX_GROUPS = 1e3;
4264
+ var DATE_BUCKET_BY_ENUM = {
4265
+ YEAR: "year",
4266
+ QUARTER: "quarter",
4267
+ MONTH: "month",
4268
+ WEEK: "week",
4269
+ DAY: "day",
4270
+ HOUR: "hour"
4271
+ };
4272
+ var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
2988
4273
  var BOOLEXP_OPS = {
2989
4274
  _eq: "eq",
2990
4275
  _neq: "neq",
@@ -3010,16 +4295,24 @@ var ORDER_DIRECTIONS = {
3010
4295
  DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
3011
4296
  };
3012
4297
  var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
3013
- function fail(message, code, candidates = []) {
4298
+ function fail2(message, code, candidates = []) {
3014
4299
  throw new DocumentCompileError(message, code, candidates);
3015
4300
  }
3016
4301
  function argsByName(field) {
3017
4302
  return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
3018
4303
  }
4304
+ function* aggregateBlockFields(selectionSet) {
4305
+ for (const node of selectionSet.selections) {
4306
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
4307
+ if (node.name.value === "__typename") continue;
4308
+ if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
4309
+ yield node;
4310
+ }
4311
+ }
3019
4312
  function assertKnownArgs(field, allowed) {
3020
4313
  for (const arg of field.arguments ?? []) {
3021
4314
  if (!allowed.includes(arg.name.value)) {
3022
- fail(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
4315
+ fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
3023
4316
  }
3024
4317
  }
3025
4318
  }
@@ -3032,8 +4325,11 @@ var OperationLowerer = class {
3032
4325
  meta(table) {
3033
4326
  return this.ctx.tables[table];
3034
4327
  }
4328
+ /** A data column — cell-metadata columns are reachable only through `cells { … }`. */
3035
4329
  column(table, name) {
3036
- return this.meta(table).columns.find((c) => c.name === name);
4330
+ const meta = this.meta(table);
4331
+ const column = meta.columns.find((c) => c.name === name);
4332
+ return column && !isCellColumn(meta, name) ? column : void 0;
3037
4333
  }
3038
4334
  link(table, name) {
3039
4335
  return this.meta(table).links.find((l) => l.name === name);
@@ -3041,7 +4337,8 @@ var OperationLowerer = class {
3041
4337
  fieldCandidates(table) {
3042
4338
  const meta = this.meta(table);
3043
4339
  return [
3044
- ...meta.columns.map((c) => c.name),
4340
+ ...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
4341
+ ...meta.cells ? [CELLS_FIELD] : [],
3045
4342
  ...meta.links.map((l) => l.name),
3046
4343
  ...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
3047
4344
  ];
@@ -3050,7 +4347,7 @@ var OperationLowerer = class {
3050
4347
  slot(name, input, table) {
3051
4348
  const variable = this.variables.get(name);
3052
4349
  if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
3053
- fail(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
4350
+ fail2(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
3054
4351
  }
3055
4352
  return name;
3056
4353
  }
@@ -3075,14 +4372,14 @@ var OperationLowerer = class {
3075
4372
  const items = node.values.map((item) => {
3076
4373
  const lowered = this.lowerValue(item);
3077
4374
  if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
3078
- fail("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
4375
+ fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
3079
4376
  }
3080
4377
  return lowered.value;
3081
4378
  });
3082
4379
  return { kind: "literal", value: items };
3083
4380
  }
3084
4381
  case graphql.Kind.OBJECT:
3085
- fail("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
4382
+ fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
3086
4383
  }
3087
4384
  }
3088
4385
  // -------------------------------------------------------------------------
@@ -3092,7 +4389,7 @@ var OperationLowerer = class {
3092
4389
  if (node.kind === graphql.Kind.VARIABLE) {
3093
4390
  return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
3094
4391
  }
3095
- if (node.kind !== graphql.Kind.OBJECT) fail(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
4392
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
3096
4393
  const operands = [];
3097
4394
  for (const field of node.fields) {
3098
4395
  const key = field.name.value;
@@ -3123,45 +4420,45 @@ var OperationLowerer = class {
3123
4420
  continue;
3124
4421
  }
3125
4422
  }
3126
- fail(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
4423
+ fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
3127
4424
  }
3128
4425
  if (operands.length === 0) return CONST_TRUE;
3129
4426
  return operands.length === 1 ? operands[0] : { kind: "and", operands };
3130
4427
  }
3131
4428
  lowerComparison(node, column, table) {
3132
4429
  if (node.kind !== graphql.Kind.OBJECT) {
3133
- fail(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
4430
+ fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
3134
4431
  }
3135
4432
  return node.fields.map((field) => {
3136
4433
  const op = BOOLEXP_OPS[field.name.value];
3137
4434
  if (!op) {
3138
- fail(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
4435
+ fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
3139
4436
  }
3140
4437
  return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
3141
4438
  });
3142
4439
  }
3143
4440
  /** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
3144
4441
  lowerAggPredicate(node, link, target) {
3145
- if (node.kind !== graphql.Kind.OBJECT) fail(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
4442
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
3146
4443
  const countField = node.fields.find((f) => f.name.value === "count");
3147
4444
  if (!countField || node.fields.length !== 1) {
3148
- fail(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
4445
+ fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
3149
4446
  }
3150
- if (countField.value.kind !== graphql.Kind.OBJECT) fail(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
4447
+ if (countField.value.kind !== graphql.Kind.OBJECT) fail2(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
3151
4448
  let predicate;
3152
4449
  let where;
3153
4450
  for (const field of countField.value.fields) {
3154
4451
  if (field.name.value === "predicate") {
3155
4452
  if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
3156
- fail(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
4453
+ fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
3157
4454
  }
3158
4455
  const opField = field.value.fields[0];
3159
4456
  const opName = opField.name.value;
3160
4457
  if (!AGG_PREDICATE_OPS.includes(opName)) {
3161
- fail(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
4458
+ fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
3162
4459
  }
3163
4460
  if (opField.value.kind !== graphql.Kind.INT) {
3164
- fail(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
4461
+ fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
3165
4462
  }
3166
4463
  predicate = { op: opName.slice(1), value: Number(opField.value.value) };
3167
4464
  continue;
@@ -3170,9 +4467,9 @@ var OperationLowerer = class {
3170
4467
  where = this.lowerBoolExp(field.value, target);
3171
4468
  continue;
3172
4469
  }
3173
- fail(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
4470
+ fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
3174
4471
  }
3175
- if (!predicate) fail(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
4472
+ if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
3176
4473
  return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
3177
4474
  }
3178
4475
  lowerOrderBy(node, table) {
@@ -3182,7 +4479,7 @@ var OperationLowerer = class {
3182
4479
  const items = node.kind === graphql.Kind.LIST ? node.values : [node];
3183
4480
  const lowered = [];
3184
4481
  for (const item of items) {
3185
- if (item.kind !== graphql.Kind.OBJECT) fail(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
4482
+ if (item.kind !== graphql.Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
3186
4483
  for (const field of item.fields) {
3187
4484
  const key = field.name.value;
3188
4485
  const column = this.column(table, key);
@@ -3193,25 +4490,25 @@ var OperationLowerer = class {
3193
4490
  const link = this.link(table, key);
3194
4491
  if (link && link.kind !== "toMany") {
3195
4492
  if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
3196
- fail(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
4493
+ fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
3197
4494
  }
3198
4495
  const inner = field.value.fields[0];
3199
4496
  const targetColumn = this.column(link.target, inner.name.value);
3200
4497
  if (!targetColumn) {
3201
- fail(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
4498
+ fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
3202
4499
  }
3203
4500
  lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
3204
4501
  continue;
3205
4502
  }
3206
- fail(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
4503
+ fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
3207
4504
  }
3208
4505
  }
3209
4506
  return lowered;
3210
4507
  }
3211
4508
  lowerDirection(node, table, column) {
3212
- if (node.kind !== graphql.Kind.ENUM) fail(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
4509
+ if (node.kind !== graphql.Kind.ENUM) fail2(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
3213
4510
  const direction = ORDER_DIRECTIONS[node.value];
3214
- if (!direction) fail(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
4511
+ if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
3215
4512
  return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
3216
4513
  }
3217
4514
  lowerDistinctOn(node, table) {
@@ -3220,9 +4517,9 @@ var OperationLowerer = class {
3220
4517
  }
3221
4518
  const items = node.kind === graphql.Kind.LIST ? node.values : [node];
3222
4519
  return items.map((item) => {
3223
- if (item.kind !== graphql.Kind.ENUM) fail(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
4520
+ if (item.kind !== graphql.Kind.ENUM) fail2(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
3224
4521
  if (!this.column(table, item.value)) {
3225
- fail(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
4522
+ fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3226
4523
  }
3227
4524
  return item.value;
3228
4525
  });
@@ -3233,16 +4530,16 @@ var OperationLowerer = class {
3233
4530
  if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
3234
4531
  if (node.kind === graphql.Kind.INT) {
3235
4532
  const value = Number(node.value);
3236
- if (value < 1) fail("limit must be a positive integer", "invalid-limit");
4533
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
3237
4534
  return { ...base, value: Math.min(value, MAX_LIMIT) };
3238
4535
  }
3239
- fail("limit must be an integer literal or a variable", "invalid-limit");
4536
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
3240
4537
  }
3241
4538
  lowerOffset(node) {
3242
4539
  if (!node) return void 0;
3243
4540
  if (node.kind === graphql.Kind.VARIABLE) return { variable: node.name.value };
3244
4541
  if (node.kind === graphql.Kind.INT) return { value: Number(node.value) };
3245
- fail("offset must be an integer literal or a variable", "invalid-offset");
4542
+ fail2("offset must be an integer literal or a variable", "invalid-offset");
3246
4543
  }
3247
4544
  // -------------------------------------------------------------------------
3248
4545
  // Selections
@@ -3251,9 +4548,9 @@ var OperationLowerer = class {
3251
4548
  const meta = this.meta(table);
3252
4549
  const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
3253
4550
  for (const node of selectionSet.selections) {
3254
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
4551
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3255
4552
  if (node.directives && node.directives.length > 0) {
3256
- fail(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
4553
+ fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
3257
4554
  }
3258
4555
  const name = node.name.value;
3259
4556
  if (name === "__typename") continue;
@@ -3261,14 +4558,19 @@ var OperationLowerer = class {
3261
4558
  const column = this.column(table, name);
3262
4559
  if (column) {
3263
4560
  if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
3264
- fail(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
4561
+ fail2(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
3265
4562
  }
3266
4563
  selection.columns.push({ alias, column: name });
3267
4564
  continue;
3268
4565
  }
4566
+ if (name === CELLS_FIELD && meta.cells) {
4567
+ if (selection.cells) fail2(`'${CELLS_FIELD}' may be selected once per object`, "invalid-selection");
4568
+ selection.cells = this.lowerCells(node, table, alias);
4569
+ continue;
4570
+ }
3269
4571
  const link = this.link(table, name);
3270
4572
  if (link) {
3271
- if (!node.selectionSet) fail(`link '${table}.${name}' needs a selection set`, "invalid-selection");
4573
+ if (!node.selectionSet) fail2(`link '${table}.${name}' needs a selection set`, "invalid-selection");
3272
4574
  const relation = {
3273
4575
  alias,
3274
4576
  link: name,
@@ -3287,7 +4589,7 @@ var OperationLowerer = class {
3287
4589
  if (offset) relation.offset = offset;
3288
4590
  }
3289
4591
  } else if (node.arguments && node.arguments.length > 0) {
3290
- fail(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
4592
+ fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
3291
4593
  }
3292
4594
  selection.relations.push(relation);
3293
4595
  continue;
@@ -3295,7 +4597,7 @@ var OperationLowerer = class {
3295
4597
  if (name.endsWith("Aggregate")) {
3296
4598
  const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
3297
4599
  if (aggLink && aggLink.kind === "toMany") {
3298
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4600
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3299
4601
  assertKnownArgs(node, ["where"]);
3300
4602
  const whereArg = argsByName(node).get("where");
3301
4603
  const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
@@ -3308,70 +4610,182 @@ var OperationLowerer = class {
3308
4610
  continue;
3309
4611
  }
3310
4612
  }
3311
- fail(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
4613
+ fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
4614
+ }
4615
+ return selection;
4616
+ }
4617
+ /**
4618
+ * `cells { <column> { at by … } }` — typed per-cell metadata. Plain tables
4619
+ * offer `at`/`by`; resolved tables add the winning `source` and newer
4620
+ * `conflicts { source at value }`.
4621
+ */
4622
+ lowerCells(node, table, alias) {
4623
+ const meta = this.meta(table);
4624
+ const cells = meta.cells ?? {};
4625
+ if (node.arguments && node.arguments.length > 0) fail2(`'${CELLS_FIELD}' takes no arguments`, "invalid-selection");
4626
+ if (!node.selectionSet) fail2(`'${CELLS_FIELD}' needs a selection set`, "invalid-selection");
4627
+ const fields = [];
4628
+ for (const cellNode of fieldNodes(node.selectionSet)) {
4629
+ const column = cellNode.name.value;
4630
+ const parts = cells[column];
4631
+ if (!parts || parts.at === void 0 && parts.source === void 0) {
4632
+ fail2(`unknown cell '${column}' on '${table}'`, "unknown-field", Object.keys(cells));
4633
+ }
4634
+ if (!cellNode.selectionSet) fail2(`cell '${column}' needs a selection set`, "invalid-selection");
4635
+ const resolved = parts.source !== void 0;
4636
+ const allowed = resolved ? ["source", "at", "by", "conflicts"] : ["at", "by"];
4637
+ const selected = [];
4638
+ for (const partNode of fieldNodes(cellNode.selectionSet)) {
4639
+ const part = partNode.name.value;
4640
+ if (!allowed.includes(part)) fail2(`unknown field '${part}' on cell '${column}'`, "unknown-field", allowed);
4641
+ const partAlias = partNode.alias?.value ?? part;
4642
+ if (part !== "conflicts") {
4643
+ if (partNode.selectionSet) fail2(`cell field '${part}' takes no sub-selection`, "invalid-selection");
4644
+ selected.push({ alias: partAlias, part });
4645
+ continue;
4646
+ }
4647
+ if (!partNode.selectionSet) fail2(`'conflicts' needs a selection set`, "invalid-selection");
4648
+ const conflictFields = fieldNodes(partNode.selectionSet).map((fieldNode) => {
4649
+ const field = fieldNode.name.value;
4650
+ if (field !== "source" && field !== "at" && field !== "value") fail2(`unknown field '${field}' on a cell conflict`, "unknown-field", ["source", "at", "value"]);
4651
+ return { alias: fieldNode.alias?.value ?? field, field };
4652
+ });
4653
+ selected.push({ alias: partAlias, part: "conflicts", fields: conflictFields, typename: CELL_TYPENAMES.conflict });
4654
+ }
4655
+ fields.push({
4656
+ alias: cellNode.alias?.value ?? column,
4657
+ column,
4658
+ typename: resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp,
4659
+ parts: selected
4660
+ });
4661
+ }
4662
+ return { alias, typename: cellsTypename(meta.typename), fields };
4663
+ }
4664
+ /** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
4665
+ lowerAggregateSelection(selectionSet, table) {
4666
+ const result = {};
4667
+ let sawAggregate = false;
4668
+ for (const node of aggregateBlockFields(selectionSet)) {
4669
+ const name = node.name.value;
4670
+ if (name === "nodes") {
4671
+ if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
4672
+ result.nodes = this.lowerSelection(node.selectionSet, table);
4673
+ continue;
4674
+ }
4675
+ if (name !== "aggregate") fail2(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
4676
+ if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
4677
+ sawAggregate = true;
4678
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
4679
+ Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
4680
+ }
4681
+ if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
4682
+ return result;
4683
+ }
4684
+ /**
4685
+ * The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
4686
+ * `aggregate` sub-block as the ungrouped shape (shared via
4687
+ * `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
4688
+ * `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
4689
+ */
4690
+ lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
4691
+ const groupColumns = new Set(groupBy.map((entry) => entry.column));
4692
+ const result = { key: [] };
4693
+ let sawAggregate = false;
4694
+ let sawKey = false;
4695
+ for (const node of aggregateBlockFields(selectionSet)) {
4696
+ const name = node.name.value;
4697
+ if (name === "key") {
4698
+ if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
4699
+ sawKey = true;
4700
+ if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
4701
+ result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
4702
+ continue;
4703
+ }
4704
+ if (name === "nodes") {
4705
+ fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
4706
+ }
4707
+ if (name !== "aggregate") fail2(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
4708
+ if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
4709
+ sawAggregate = true;
4710
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
4711
+ Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
4712
+ }
4713
+ const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
4714
+ if (result.key.length === 0 && !rendersAggregate) {
4715
+ fail2(
4716
+ "grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
4717
+ "invalid-aggregate",
4718
+ ["key", "aggregate"]
4719
+ );
3312
4720
  }
3313
- return selection;
4721
+ return result;
3314
4722
  }
3315
- /** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
3316
- lowerAggregateSelection(selectionSet, table) {
3317
- const result = {};
3318
- let sawAggregate = false;
4723
+ /**
4724
+ * `key { month officer }` → the projected group columns. Every field must be
4725
+ * one of the query's own group keys: any other column is indeterminate
4726
+ * within a group, so selecting it is a document error rather than an
4727
+ * arbitrary pick.
4728
+ */
4729
+ lowerGroupKeySelection(selectionSet, table, groupColumns) {
4730
+ const key = [];
3319
4731
  for (const node of selectionSet.selections) {
3320
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
4732
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3321
4733
  const name = node.name.value;
3322
4734
  if (name === "__typename") continue;
3323
- if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
3324
- if (name === "nodes") {
3325
- if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
3326
- result.nodes = this.lowerSelection(node.selectionSet, table);
3327
- continue;
4735
+ if (!this.column(table, name)) {
4736
+ fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
3328
4737
  }
3329
- if (name !== "aggregate") fail(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
3330
- if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3331
- sawAggregate = true;
3332
- if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
3333
- for (const aggField of node.selectionSet.selections) {
3334
- if (aggField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3335
- const aggName = aggField.name.value;
3336
- if (aggName === "__typename") continue;
3337
- if (aggField.alias) fail(`aliases are not supported inside aggregate blocks ('${aggField.alias.value}')`, "invalid-aggregate");
3338
- if (aggName === "count") {
3339
- assertKnownArgs(aggField, ["columns", "distinct"]);
3340
- const args = argsByName(aggField);
3341
- const count = {};
3342
- const columns = args.get("columns");
3343
- if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
3344
- const distinct = args.get("distinct");
3345
- if (distinct) {
3346
- if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
3347
- count.distinct = distinct.value.value;
3348
- }
3349
- result.count = count;
3350
- continue;
4738
+ if (!groupColumns.has(name)) {
4739
+ fail2(
4740
+ `key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
4741
+ "invalid-group-key",
4742
+ [...groupColumns]
4743
+ );
4744
+ }
4745
+ key.push({ alias: node.alias?.value ?? name, column: name });
4746
+ }
4747
+ return key;
4748
+ }
4749
+ /** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
4750
+ lowerAggregateFields(selectionSet, table) {
4751
+ const result = {};
4752
+ for (const aggField of aggregateBlockFields(selectionSet)) {
4753
+ const aggName = aggField.name.value;
4754
+ if (aggName === "count") {
4755
+ assertKnownArgs(aggField, ["columns", "distinct"]);
4756
+ const args = argsByName(aggField);
4757
+ const count = {};
4758
+ const columns = args.get("columns");
4759
+ if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
4760
+ const distinct = args.get("distinct");
4761
+ if (distinct) {
4762
+ if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
4763
+ count.distinct = distinct.value.value;
3351
4764
  }
3352
- if (aggName === "sum" || aggName === "avg") {
3353
- if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
3354
- const columns = [];
3355
- for (const columnField of aggField.selectionSet.selections) {
3356
- if (columnField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3357
- if (columnField.name.value === "__typename") continue;
3358
- const column = this.column(table, columnField.name.value);
3359
- if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
3360
- fail(
3361
- `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
3362
- "invalid-aggregate",
3363
- this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
3364
- );
3365
- }
3366
- columns.push(columnField.name.value);
4765
+ result.count = count;
4766
+ continue;
4767
+ }
4768
+ if (aggName === "sum" || aggName === "avg") {
4769
+ if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
4770
+ const columns = [];
4771
+ for (const columnField of aggField.selectionSet.selections) {
4772
+ if (columnField.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
4773
+ if (columnField.name.value === "__typename") continue;
4774
+ const column = this.column(table, columnField.name.value);
4775
+ if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
4776
+ fail2(
4777
+ `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
4778
+ "invalid-aggregate",
4779
+ this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
4780
+ );
3367
4781
  }
3368
- result[aggName] = columns;
3369
- continue;
4782
+ columns.push(columnField.name.value);
3370
4783
  }
3371
- fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
4784
+ result[aggName] = columns;
4785
+ continue;
3372
4786
  }
4787
+ fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
3373
4788
  }
3374
- if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
3375
4789
  return result;
3376
4790
  }
3377
4791
  // -------------------------------------------------------------------------
@@ -3380,32 +4794,65 @@ var OperationLowerer = class {
3380
4794
  lowerRoots(operation, kind) {
3381
4795
  const roots = [];
3382
4796
  for (const node of operation.selectionSet.selections) {
3383
- if (node.kind !== graphql.Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
4797
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
4798
+ if (node.selectionSet) graphql.visit(node.selectionSet, {
4799
+ Directive(directive) {
4800
+ if (directive.name.value === "assertAffectedRows") {
4801
+ fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
4802
+ }
4803
+ }
4804
+ });
4805
+ const assertion = this.parseAffectedRowAssertion(node, kind);
3384
4806
  const gate = this.parseRootDirectives(node);
3385
4807
  if (gate.omit) continue;
3386
- const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
3387
- roots.push(gate.includeIf ? { ...root, includeIf: gate.includeIf } : root);
4808
+ const lowered = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
4809
+ const meta = this.meta(lowered.table);
4810
+ const root = kind === "query" ? applyPrefetchDirectives({ node, root: lowered, meta }) : applyWritebackDirectives({ node, root: lowered, meta });
4811
+ const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
4812
+ roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
3388
4813
  }
3389
- if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
4814
+ if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
3390
4815
  return roots;
3391
4816
  }
4817
+ parseAffectedRowAssertion(node, kind) {
4818
+ const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
4819
+ if (!assertions.length) return void 0;
4820
+ if (kind !== "mutation" || assertions.length !== 1) {
4821
+ fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
4822
+ }
4823
+ if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
4824
+ fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
4825
+ }
4826
+ const args = assertions[0].arguments;
4827
+ const arg = args?.[0];
4828
+ if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
4829
+ fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
4830
+ }
4831
+ const count = Number(arg.value.value);
4832
+ if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
4833
+ fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
4834
+ }
4835
+ return count;
4836
+ }
3392
4837
  parseRootDirectives(node) {
3393
4838
  let includeIf;
3394
4839
  let omit = false;
3395
4840
  for (const directive of node.directives ?? []) {
3396
4841
  const name = directive.name.value;
4842
+ if (name === "assertAffectedRows") continue;
4843
+ if (SOURCE_DIRECTIVES.has(name)) continue;
3397
4844
  if (name !== "include" && name !== "skip") {
3398
- fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
4845
+ fail2(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
3399
4846
  }
3400
4847
  const ifArg = directive.arguments?.find((a) => a.name.value === "if");
3401
- if (!ifArg) fail(`@${name} needs an if: argument`, "invalid-directive");
4848
+ if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
3402
4849
  if (ifArg.value.kind === graphql.Kind.BOOLEAN) {
3403
4850
  const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
3404
4851
  if (!keep) omit = true;
3405
4852
  continue;
3406
4853
  }
3407
- if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
3408
- if (includeIf) fail("at most one of @include/@skip per root field", "invalid-directive");
4854
+ if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail2(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
4855
+ if (includeIf) fail2("at most one of @include/@skip per root field", "invalid-directive");
3409
4856
  includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
3410
4857
  }
3411
4858
  return omit ? { omit: true } : { includeIf, omit: false };
@@ -3414,12 +4861,12 @@ var OperationLowerer = class {
3414
4861
  const name = node.name.value;
3415
4862
  const alias = node.alias?.value ?? name;
3416
4863
  const rootInfo = this.ctx.queryRoots.get(name);
3417
- if (!rootInfo) fail(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
4864
+ if (!rootInfo) fail2(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
3418
4865
  const { table } = rootInfo;
3419
4866
  const args = argsByName(node);
3420
4867
  if (rootInfo.kind === "select") {
3421
4868
  assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
3422
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4869
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3423
4870
  const where = args.get("where");
3424
4871
  const orderBy = args.get("orderBy");
3425
4872
  const distinctOn = args.get("distinctOn");
@@ -3438,12 +4885,15 @@ var OperationLowerer = class {
3438
4885
  };
3439
4886
  }
3440
4887
  if (rootInfo.kind === "byPk") {
3441
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4888
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3442
4889
  const pk = this.lowerPkArgs(node, table);
3443
4890
  return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
3444
4891
  }
4892
+ if (rootInfo.kind === "aggregateGroups") {
4893
+ return this.lowerAggregateGroupsRoot(node, { alias, table });
4894
+ }
3445
4895
  assertKnownArgs(node, ["where"]);
3446
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4896
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3447
4897
  const whereArg = args.get("where");
3448
4898
  const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
3449
4899
  return {
@@ -3454,6 +4904,108 @@ var OperationLowerer = class {
3454
4904
  ...aggregate
3455
4905
  };
3456
4906
  }
4907
+ /** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
4908
+ lowerAggregateGroupsRoot(node, { alias, table }) {
4909
+ assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
4910
+ if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
4911
+ const args = argsByName(node);
4912
+ const groupByArg = args.get("groupBy");
4913
+ if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
4914
+ const whereArg = args.get("where");
4915
+ const groupBy = this.lowerGroupBy(groupByArg.value, table);
4916
+ const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
4917
+ const offset = this.lowerOffset(args.get("offset")?.value);
4918
+ return {
4919
+ kind: "aggregateGroups",
4920
+ alias,
4921
+ table,
4922
+ where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
4923
+ groupBy,
4924
+ key: aggregate.key,
4925
+ ...aggregate.count ? { count: aggregate.count } : {},
4926
+ ...aggregate.sum ? { sum: aggregate.sum } : {},
4927
+ ...aggregate.avg ? { avg: aggregate.avg } : {},
4928
+ limit: this.lowerGroupLimit(args.get("limit")?.value),
4929
+ ...offset ? { offset } : {}
4930
+ };
4931
+ }
4932
+ /**
4933
+ * `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
4934
+ *
4935
+ * Document-authored only: a whole-argument variable is rejected rather than
4936
+ * becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
4937
+ * and dynamic-input.ts refuses aggregation shapes from client input on the
4938
+ * same reasoning — `where` stays dynamic, which is what runtime filtering
4939
+ * actually needs.
4940
+ */
4941
+ lowerGroupBy(node, table) {
4942
+ if (node.kind === graphql.Kind.VARIABLE) {
4943
+ fail2(
4944
+ `groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
4945
+ "dynamic-group-by"
4946
+ );
4947
+ }
4948
+ const items = node.kind === graphql.Kind.LIST ? node.values : [node];
4949
+ if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
4950
+ const seen = /* @__PURE__ */ new Set();
4951
+ return items.map((item) => {
4952
+ if (item.kind !== graphql.Kind.OBJECT) {
4953
+ fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
4954
+ }
4955
+ const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
4956
+ for (const key of fields.keys()) {
4957
+ if (key !== "column" && key !== "bucket") {
4958
+ fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
4959
+ }
4960
+ }
4961
+ const columnNode = fields.get("column");
4962
+ if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
4963
+ if (columnNode.kind !== graphql.Kind.ENUM) {
4964
+ fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
4965
+ }
4966
+ const column = this.column(table, columnNode.value);
4967
+ if (!column) {
4968
+ fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
4969
+ }
4970
+ if (column.type === "jsonb") {
4971
+ fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
4972
+ }
4973
+ const bucketNode = fields.get("bucket");
4974
+ let bucket;
4975
+ if (bucketNode) {
4976
+ if (bucketNode.kind !== graphql.Kind.ENUM) {
4977
+ fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
4978
+ }
4979
+ bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
4980
+ if (!bucket) {
4981
+ fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
4982
+ }
4983
+ if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
4984
+ fail2(
4985
+ `groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
4986
+ "invalid-group-by"
4987
+ );
4988
+ }
4989
+ }
4990
+ if (seen.has(columnNode.value)) {
4991
+ fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
4992
+ }
4993
+ seen.add(columnNode.value);
4994
+ return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
4995
+ });
4996
+ }
4997
+ /** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
4998
+ lowerGroupLimit(node) {
4999
+ const base = { default: MAX_GROUPS, max: MAX_GROUPS };
5000
+ if (!node) return base;
5001
+ if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
5002
+ if (node.kind === graphql.Kind.INT) {
5003
+ const value = Number(node.value);
5004
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
5005
+ return { ...base, value: Math.min(value, MAX_GROUPS) };
5006
+ }
5007
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
5008
+ }
3457
5009
  /** byPk-style pk arguments: one argument per primary-key column, exactly. */
3458
5010
  lowerPkArgs(node, table) {
3459
5011
  const meta = this.meta(table);
@@ -3461,7 +5013,7 @@ var OperationLowerer = class {
3461
5013
  assertKnownArgs(node, meta.primaryKey);
3462
5014
  return meta.primaryKey.map((column) => {
3463
5015
  const arg = args.get(column);
3464
- if (!arg) fail(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
5016
+ if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
3465
5017
  return { column, value: this.lowerValue(arg.value) };
3466
5018
  });
3467
5019
  }
@@ -3472,19 +5024,22 @@ var OperationLowerer = class {
3472
5024
  const name = node.name.value;
3473
5025
  const alias = node.alias?.value ?? name;
3474
5026
  const rootInfo = this.ctx.mutationRoots.get(name);
3475
- if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
5027
+ if (!rootInfo) fail2(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
3476
5028
  if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
3477
5029
  return this.lowerCowRoot(node, rootInfo);
3478
5030
  }
5031
+ if (rootInfo.op === "resolvedWrite") {
5032
+ return this.lowerResolvedRoot(node, rootInfo);
5033
+ }
3479
5034
  const { op, table } = rootInfo;
3480
5035
  const args = argsByName(node);
3481
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
5036
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3482
5037
  const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
3483
5038
  switch (op) {
3484
5039
  case "insert": {
3485
5040
  assertKnownArgs(node, ["objects", "onConflict"]);
3486
5041
  const objects = args.get("objects");
3487
- if (!objects) fail(`'${name}' needs objects:`, "missing-argument");
5042
+ if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
3488
5043
  const response = this.lowerMutationResponse(node.selectionSet, table);
3489
5044
  return {
3490
5045
  ...base,
@@ -3496,7 +5051,7 @@ var OperationLowerer = class {
3496
5051
  case "insertOne": {
3497
5052
  assertKnownArgs(node, ["object", "onConflict"]);
3498
5053
  const object = args.get("object");
3499
- if (!object) fail(`'${name}' needs object:`, "missing-argument");
5054
+ if (!object) fail2(`'${name}' needs object:`, "missing-argument");
3500
5055
  return {
3501
5056
  ...base,
3502
5057
  objects: this.lowerInsertObjects(object.value, table, { list: false }),
@@ -3507,7 +5062,7 @@ var OperationLowerer = class {
3507
5062
  case "update": {
3508
5063
  assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
3509
5064
  const where = args.get("where");
3510
- if (!where) fail(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
5065
+ if (!where) fail2(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
3511
5066
  return {
3512
5067
  ...base,
3513
5068
  ...this.lowerMutationResponse(node.selectionSet, table),
@@ -3518,7 +5073,7 @@ var OperationLowerer = class {
3518
5073
  case "updateByPk": {
3519
5074
  assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
3520
5075
  const pkColumns = args.get("pkColumns");
3521
- if (!pkColumns) fail(`'${name}' needs pkColumns:`, "missing-argument");
5076
+ if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
3522
5077
  return {
3523
5078
  ...base,
3524
5079
  pk: this.lowerPkColumnsArg(pkColumns.value, table),
@@ -3529,7 +5084,7 @@ var OperationLowerer = class {
3529
5084
  case "delete": {
3530
5085
  assertKnownArgs(node, ["where"]);
3531
5086
  const where = args.get("where");
3532
- if (!where) fail(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
5087
+ if (!where) fail2(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
3533
5088
  return {
3534
5089
  ...base,
3535
5090
  ...this.lowerMutationResponse(node.selectionSet, table),
@@ -3556,11 +5111,11 @@ var OperationLowerer = class {
3556
5111
  const name = node.name.value;
3557
5112
  const alias = node.alias?.value ?? name;
3558
5113
  const { table } = rootInfo;
3559
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
5114
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3560
5115
  const meta = this.meta(table);
3561
5116
  const args = argsByName(node);
3562
5117
  const pkArg = args.get(rootInfo.pk);
3563
- if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
5118
+ if (!pkArg) fail2(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
3564
5119
  const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
3565
5120
  const base = {
3566
5121
  kind: "mutation",
@@ -3581,24 +5136,78 @@ var OperationLowerer = class {
3581
5136
  const entries = rootInfo.set.map((column) => {
3582
5137
  const arg = args.get(column);
3583
5138
  if (!arg) {
3584
- fail(
5139
+ fail2(
3585
5140
  `'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
3586
5141
  "missing-argument",
3587
5142
  [...rootInfo.set]
3588
5143
  );
3589
5144
  }
3590
5145
  if (!meta.settable.includes(column)) {
3591
- fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
5146
+ fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
3592
5147
  }
3593
5148
  return { column, value: this.lowerValue(arg.value) };
3594
5149
  });
3595
5150
  return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
3596
5151
  }
5152
+ /**
5153
+ * Resolved-table roots: insertOne(object), updateByPk(pkColumns, _set),
5154
+ * deleteByPk(pk…), revertByPk(pkColumns, columns). The executor writes only
5155
+ * the touched cells into the manual source; `returning` reads the view.
5156
+ */
5157
+ lowerResolvedRoot(node, rootInfo) {
5158
+ const name = node.name.value;
5159
+ const alias = node.alias?.value ?? name;
5160
+ const { table, action } = rootInfo;
5161
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
5162
+ const args = argsByName(node);
5163
+ const base = {
5164
+ kind: "mutation",
5165
+ op: "resolvedWrite",
5166
+ resolvedAction: action,
5167
+ alias,
5168
+ table,
5169
+ pinnedColumns: [],
5170
+ affectedRows: false,
5171
+ returning: this.lowerSelection(node.selectionSet, table)
5172
+ };
5173
+ switch (action) {
5174
+ case "insert": {
5175
+ assertKnownArgs(node, ["object"]);
5176
+ const object = args.get("object");
5177
+ if (!object) fail2(`'${name}' needs object:`, "missing-argument");
5178
+ return { ...base, objects: this.lowerInsertObjects(object.value, table, { list: false }) };
5179
+ }
5180
+ case "update": {
5181
+ assertKnownArgs(node, ["pkColumns", "_set"]);
5182
+ const pkColumns = args.get("pkColumns");
5183
+ if (!pkColumns || !args.get("_set")) fail2(`'${name}' needs pkColumns: and _set:`, "missing-argument");
5184
+ return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), ...this.lowerWriteValues(args, table) };
5185
+ }
5186
+ case "delete":
5187
+ return { ...base, pk: this.lowerPkArgs(node, table) };
5188
+ case "revert": {
5189
+ assertKnownArgs(node, ["pkColumns", "columns"]);
5190
+ const pkColumns = args.get("pkColumns");
5191
+ const columns = args.get("columns");
5192
+ if (!pkColumns || !columns) fail2(`'${name}' needs pkColumns: and columns:`, "missing-argument");
5193
+ const items = columns.value.kind === graphql.Kind.LIST ? columns.value.values : [columns.value];
5194
+ const meta = this.meta(table);
5195
+ const revertColumns = items.map((item) => {
5196
+ if (item.kind !== graphql.Kind.ENUM || !meta.settable.includes(item.value)) {
5197
+ fail2(`'${name}' columns: must list update-column enum values written in the document`, "invalid-revert-columns", meta.settable);
5198
+ }
5199
+ return item.value;
5200
+ });
5201
+ if (revertColumns.length === 0) fail2(`'${name}' columns: must name at least one column`, "invalid-revert-columns");
5202
+ return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), revertColumns: [...new Set(revertColumns)] };
5203
+ }
5204
+ }
5205
+ }
3597
5206
  lowerMutationResponse(selectionSet, table) {
3598
5207
  let affectedRows = false;
3599
5208
  let returning = null;
3600
5209
  for (const node of selectionSet.selections) {
3601
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
5210
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3602
5211
  const name = node.name.value;
3603
5212
  if (name === "__typename") continue;
3604
5213
  if (name === "affectedRows") {
@@ -3606,34 +5215,34 @@ var OperationLowerer = class {
3606
5215
  continue;
3607
5216
  }
3608
5217
  if (name === "returning") {
3609
- if (!node.selectionSet) fail("'returning' needs a selection set", "invalid-selection");
5218
+ if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
3610
5219
  returning = this.lowerSelection(node.selectionSet, table);
3611
5220
  continue;
3612
5221
  }
3613
- fail(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
5222
+ fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
3614
5223
  }
3615
5224
  return { affectedRows, returning };
3616
5225
  }
3617
5226
  lowerInsertObjects(node, table, { list }) {
3618
5227
  if (node.kind === graphql.Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
3619
5228
  if (list) {
3620
- if (node.kind !== graphql.Kind.LIST) fail(`objects: must be a list literal or a variable`, "invalid-insert");
5229
+ if (node.kind !== graphql.Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
3621
5230
  return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
3622
5231
  }
3623
5232
  return { rows: [this.lowerInsertRow(node, table)] };
3624
5233
  }
3625
5234
  lowerInsertRow(node, table) {
3626
- if (node.kind !== graphql.Kind.OBJECT) fail(`insert rows on '${table}' must be objects`, "invalid-insert");
5235
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
3627
5236
  const meta = this.meta(table);
3628
5237
  return node.fields.map((field) => {
3629
5238
  const key = field.name.value;
3630
5239
  if (this.link(table, key)) {
3631
- fail(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
5240
+ fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
3632
5241
  }
3633
5242
  if (!meta.insertable.includes(key)) {
3634
5243
  const column = this.column(table, key);
3635
- if (column) fail(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
3636
- fail(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
5244
+ if (column) fail2(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
5245
+ fail2(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
3637
5246
  }
3638
5247
  return { column: key, value: this.lowerValue(field.value) };
3639
5248
  });
@@ -3642,32 +5251,32 @@ var OperationLowerer = class {
3642
5251
  if (!arg) return {};
3643
5252
  const node = arg.value;
3644
5253
  if (node.kind === graphql.Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
3645
- if (node.kind !== graphql.Kind.OBJECT) fail("onConflict: must be an object literal or a variable", "invalid-on-conflict");
5254
+ if (node.kind !== graphql.Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
3646
5255
  const meta = this.meta(table);
3647
5256
  let constraint;
3648
5257
  const updateColumns = [];
3649
5258
  for (const field of node.fields) {
3650
5259
  if (field.name.value === "constraint") {
3651
- if (field.value.kind !== graphql.Kind.ENUM) fail("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
5260
+ if (field.value.kind !== graphql.Kind.ENUM) fail2("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
3652
5261
  constraint = field.value.value;
3653
5262
  continue;
3654
5263
  }
3655
5264
  if (field.name.value === "updateColumns") {
3656
5265
  const items = field.value.kind === graphql.Kind.LIST ? field.value.values : [field.value];
3657
5266
  for (const item of items) {
3658
- if (item.kind !== graphql.Kind.ENUM) fail("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
5267
+ if (item.kind !== graphql.Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
3659
5268
  if (!meta.settable.includes(item.value)) {
3660
- fail(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
5269
+ fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
3661
5270
  }
3662
5271
  updateColumns.push(item.value);
3663
5272
  }
3664
5273
  continue;
3665
5274
  }
3666
- fail(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
5275
+ fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
3667
5276
  }
3668
- if (!constraint) fail("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
5277
+ if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
3669
5278
  if (!meta.constraints.some((c) => c.name === constraint)) {
3670
- fail(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
5279
+ fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
3671
5280
  }
3672
5281
  return { onConflict: { constraint, updateColumns } };
3673
5282
  }
@@ -3685,36 +5294,36 @@ var OperationLowerer = class {
3685
5294
  const inc = args.get("_inc");
3686
5295
  if (inc) {
3687
5296
  if (inc.value.kind === graphql.Kind.VARIABLE) {
3688
- fail("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
5297
+ fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
3689
5298
  }
3690
5299
  const numeric4 = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
3691
5300
  result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric4, "_inc") };
3692
5301
  }
3693
5302
  if (!result.set && !result.append && !result.inc) {
3694
- fail(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
5303
+ fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
3695
5304
  }
3696
5305
  return result;
3697
5306
  }
3698
5307
  lowerColumnEntries(node, table, whitelist, argName) {
3699
- if (node.kind !== graphql.Kind.OBJECT) fail(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
5308
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
3700
5309
  return node.fields.map((field) => {
3701
5310
  const key = field.name.value;
3702
5311
  if (!whitelist.includes(key)) {
3703
- fail(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
5312
+ fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
3704
5313
  }
3705
5314
  return { column: key, value: this.lowerValue(field.value) };
3706
5315
  });
3707
5316
  }
3708
5317
  lowerPkColumnsArg(node, table) {
3709
- if (node.kind !== graphql.Kind.OBJECT) fail(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
5318
+ if (node.kind !== graphql.Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
3710
5319
  const meta = this.meta(table);
3711
5320
  const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
3712
5321
  for (const key of provided.keys()) {
3713
- if (!meta.primaryKey.includes(key)) fail(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
5322
+ if (!meta.primaryKey.includes(key)) fail2(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
3714
5323
  }
3715
5324
  return meta.primaryKey.map((column) => {
3716
5325
  const value = provided.get(column);
3717
- if (!value) fail(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
5326
+ if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
3718
5327
  return { column, value: this.lowerValue(value) };
3719
5328
  });
3720
5329
  }
@@ -3723,6 +5332,19 @@ function lowerOperation({ restricted, ctx }) {
3723
5332
  const lowerer = new OperationLowerer(ctx, restricted);
3724
5333
  return lowerer.lowerRoots(restricted.definition, restricted.kind);
3725
5334
  }
5335
+ function isCellColumn(meta, name) {
5336
+ if (!meta.cells) return false;
5337
+ return Object.values(meta.cells).some((parts) => Object.values(parts).includes(name));
5338
+ }
5339
+ function fieldNodes(selectionSet) {
5340
+ const out = [];
5341
+ for (const node of selectionSet.selections) {
5342
+ if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
5343
+ if (node.directives && node.directives.length > 0) fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
5344
+ if (node.name.value !== "__typename") out.push(node);
5345
+ }
5346
+ return out;
5347
+ }
3726
5348
  function parseSdl(sdl) {
3727
5349
  return graphql.buildSchema(sdl);
3728
5350
  }
@@ -3963,7 +5585,7 @@ function compileNamespace({ canonical, sdl, documents }) {
3963
5585
  const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
3964
5586
  const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
3965
5587
  operations[restricted.name] = {
3966
- irVersion: IR_VERSION,
5588
+ irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
3967
5589
  name: restricted.name,
3968
5590
  opHash: computeOpHash({ definition: flattened, schemaHash }),
3969
5591
  kind: restricted.kind,
@@ -3990,7 +5612,7 @@ function compileNamespace({ canonical, sdl, documents }) {
3990
5612
  }
3991
5613
  if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
3992
5614
  const artifact = {
3993
- irVersion: IR_VERSION,
5615
+ irVersion: requiredArtifactIrVersion(Object.values(operations)),
3994
5616
  namespace: ctx.namespace,
3995
5617
  schemaHash,
3996
5618
  opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
@@ -4099,15 +5721,6 @@ function columnWireType(column) {
4099
5721
  const notNull = column.primary_key === true || column.nullable === false;
4100
5722
  return notNull || base === "unknown" ? base : `${base} | null`;
4101
5723
  }
4102
-
4103
- // src/compiler/sql.ts
4104
- var CLAIMS_SETTING = "request.jwt.claims";
4105
- var CAPABILITIES_CLAIM_KEY = "capabilities";
4106
- var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
4107
- var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
4108
- var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
4109
- var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
4110
- var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
4111
5724
  var identifierSchema = zod.z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
4112
5725
  var namespaceSchema = zod.z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
4113
5726
  var tableReferenceSchema = zod.z.string().refine(
@@ -4190,12 +5803,12 @@ function castParts(cast) {
4190
5803
 
4191
5804
  // src/predicate/ast.ts
4192
5805
  var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
4193
- function walkPredicate(predicate, visit) {
4194
- visit(predicate);
5806
+ function walkPredicate(predicate, visit2) {
5807
+ visit2(predicate);
4195
5808
  if (predicate.kind === "and" || predicate.kind === "or") {
4196
- for (const operand of predicate.operands) walkPredicate(operand, visit);
5809
+ for (const operand of predicate.operands) walkPredicate(operand, visit2);
4197
5810
  }
4198
- if (predicate.kind === "not") walkPredicate(predicate.operand, visit);
5811
+ if (predicate.kind === "not") walkPredicate(predicate.operand, visit2);
4199
5812
  }
4200
5813
  function predicateOperands(predicate) {
4201
5814
  const operands = [];
@@ -4224,7 +5837,7 @@ var OPERATOR_PATTERN2 = /^(!=|<=|>=|<>|=|<|>)/;
4224
5837
  var IDENTIFIER_START2 = /[a-z_]/;
4225
5838
  var IDENTIFIER_CHAR2 = /[a-z0-9_.-]/;
4226
5839
  var NUMBER_PATTERN2 = /^-?\d+(\.\d+)?/;
4227
- function tokenize2(source) {
5840
+ function tokenize3(source) {
4228
5841
  const tokens = [];
4229
5842
  let position = 0;
4230
5843
  while (position < source.length) {
@@ -4298,7 +5911,7 @@ function readString2(source, start) {
4298
5911
  }
4299
5912
  throw new PredicateParseError("unterminated string literal", start);
4300
5913
  }
4301
- var Parser2 = class {
5914
+ var Parser3 = class {
4302
5915
  constructor(tokens) {
4303
5916
  this.tokens = tokens;
4304
5917
  }
@@ -4430,7 +6043,7 @@ var Parser2 = class {
4430
6043
  function parsePredicate(source) {
4431
6044
  const trimmed = source.trim();
4432
6045
  if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
4433
- return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
6046
+ return { source: trimmed, ast: new Parser3(tokenize3(trimmed)).parse() };
4434
6047
  }
4435
6048
 
4436
6049
  // src/schema/predicates.ts
@@ -5152,6 +6765,88 @@ function snapshotTableNames(snapshot, schema) {
5152
6765
  return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
5153
6766
  }
5154
6767
 
6768
+ // src/engine/materialized-refresh.ts
6769
+ function materializedRefreshFunctions(canonical) {
6770
+ const schema = ormSchemaName(canonical.namespace);
6771
+ const functions = /* @__PURE__ */ new Map();
6772
+ for (const view of canonical.views ?? []) {
6773
+ const materialized = view.resolved?.materialized;
6774
+ if (!materialized) continue;
6775
+ const table = canonical.tables.find((candidate) => candidate.table === materialized.table);
6776
+ const pk = view.primaryKey ?? [];
6777
+ if (!table || pk.length === 0) {
6778
+ throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
6779
+ }
6780
+ functions.set(view.view, refreshFunctionSql({
6781
+ schema,
6782
+ name: view.view,
6783
+ table: materialized.table,
6784
+ compute: materialized.compute,
6785
+ columns: table.columns.map((column) => column.columnName),
6786
+ pk
6787
+ }));
6788
+ }
6789
+ return functions;
6790
+ }
6791
+ function refreshFunctionSql({ schema, name, table, compute, columns, pk }) {
6792
+ const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name))}`;
6793
+ const target = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
6794
+ const source = `${quoteIdentifier(schema)}.${quoteIdentifier(compute)}`;
6795
+ const list = columns.map(quoteIdentifier).join(", ");
6796
+ const others = columns.filter((column) => !pk.includes(column));
6797
+ const keys = pk.map(quoteIdentifier).join(", ");
6798
+ const matches = pk.map((column) => `c.${quoteIdentifier(column)} = m.${quoteIdentifier(column)}`).join(" AND ");
6799
+ const onConflict = others.length === 0 ? "DO NOTHING" : `DO UPDATE SET ${others.map((column) => `${quoteIdentifier(column)} = EXCLUDED.${quoteIdentifier(column)}`).join(", ")}
6800
+ WHERE (${others.map((column) => `m.${quoteIdentifier(column)}`).join(", ")}) IS DISTINCT FROM (${others.map((column) => `EXCLUDED.${quoteIdentifier(column)}`).join(", ")})`;
6801
+ const lockKey = quoteLiteral(materializeLockKey(schema, name));
6802
+ return `CREATE OR REPLACE FUNCTION ${fn}(wait boolean DEFAULT false)
6803
+ RETURNS TABLE(upserted bigint, deleted bigint, locked boolean)
6804
+ LANGUAGE plpgsql
6805
+ SECURITY INVOKER
6806
+ SET search_path = pg_catalog, pg_temp
6807
+ AS $orm_refresh$
6808
+ #variable_conflict use_column
6809
+ DECLARE
6810
+ caller_claims text := current_setting('request.jwt.claims', true);
6811
+ BEGIN
6812
+ IF wait THEN
6813
+ PERFORM pg_advisory_xact_lock(hashtext(${lockKey}));
6814
+ ELSIF NOT pg_try_advisory_xact_lock(hashtext(${lockKey})) THEN
6815
+ RETURN QUERY SELECT 0::bigint, 0::bigint, false;
6816
+ RETURN;
6817
+ END IF;
6818
+ PERFORM set_config('request.jwt.claims', ${quoteLiteral(MATERIALIZE_CLAIMS_JSON)}, true);
6819
+ RETURN QUERY
6820
+ WITH up AS (
6821
+ INSERT INTO ${target} AS m (${list})
6822
+ SELECT ${list} FROM ${source}
6823
+ ON CONFLICT (${keys}) ${onConflict}
6824
+ RETURNING 1
6825
+ ), gone AS (
6826
+ DELETE FROM ${target} AS m
6827
+ WHERE NOT EXISTS (SELECT 1 FROM ${source} AS c WHERE ${matches})
6828
+ RETURNING 1
6829
+ )
6830
+ SELECT (SELECT count(*) FROM up), (SELECT count(*) FROM gone), true;
6831
+ -- NULL resets to the session value when the caller had none.
6832
+ PERFORM set_config('request.jwt.claims', caller_claims, true);
6833
+ END
6834
+ $orm_refresh$`;
6835
+ }
6836
+ function refreshFunctionStatements({ namespace, previous, current }) {
6837
+ const schema = quoteIdentifier(ormSchemaName(namespace));
6838
+ const statements = [];
6839
+ for (const name of Object.keys(previous).sort()) {
6840
+ if (!current.has(name)) statements.push(`DROP FUNCTION IF EXISTS ${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`);
6841
+ }
6842
+ for (const [name, create] of [...current].sort(([a], [b]) => a.localeCompare(b))) {
6843
+ if (previous[name] === create) continue;
6844
+ const fn = `${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`;
6845
+ statements.push(create, `REVOKE ALL ON FUNCTION ${fn} FROM PUBLIC`);
6846
+ }
6847
+ return statements;
6848
+ }
6849
+
5155
6850
  // src/engine/canonical2.ts
5156
6851
  var FACTORIES2 = {
5157
6852
  uuid: (name) => pgCore.uuid(name),
@@ -5170,7 +6865,7 @@ var FACTORIES2 = {
5170
6865
  var SCHEMA_IMPORT_KEY2 = "$schema";
5171
6866
  var enumImportKey = (name) => `$enum_${name}`;
5172
6867
  var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
5173
- function buildDrizzleEntitiesV2(definition) {
6868
+ function buildDrizzleEntitiesV2(definition, options = {}) {
5174
6869
  const schemaName = ormSchemaName(definition.namespace);
5175
6870
  const schema = pgCore.pgSchema(schemaName);
5176
6871
  const imports = { [SCHEMA_IMPORT_KEY2]: schema };
@@ -5185,11 +6880,19 @@ function buildDrizzleEntitiesV2(definition) {
5185
6880
  }
5186
6881
  const byName = new Map(definition.tables.map((table) => [table.table, table]));
5187
6882
  for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
5188
- const drizzleTable = buildTable2(schema, table, built, enums, byName);
6883
+ const drizzleTable = buildTable2(schema, table, built, enums, byName, options.introspection ?? false);
5189
6884
  built.set(table.table, drizzleTable);
5190
6885
  imports[table.table] = drizzleTable;
5191
6886
  tables[table.table] = drizzleTable;
5192
6887
  }
6888
+ if (options.introspection) {
6889
+ for (const table of definition.tables.filter((table2) => table2.source)) {
6890
+ const columns = Object.fromEntries(
6891
+ table.columns.map((column) => [column.columnName, buildViewColumnV2({ view: table.table, column, enums })])
6892
+ );
6893
+ imports[table.table] = schema.view(table.table, columns).existing();
6894
+ }
6895
+ }
5193
6896
  for (const view of definition.views ?? []) {
5194
6897
  const columns = Object.fromEntries(
5195
6898
  view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
@@ -5433,9 +7136,12 @@ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, b
5433
7136
  function generateSnapshotV2(definition, previousId3) {
5434
7137
  const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
5435
7138
  const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
5436
- if ((definition.views?.length ?? 0) > 0) {
7139
+ if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
5437
7140
  const extended = snapshot;
7141
+ const refreshFunctions = materializedRefreshFunctions(definition);
5438
7142
  extended._sequenceOrm = {
7143
+ ...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
7144
+ ...refreshFunctions.size > 0 ? { refreshFunctions: Object.fromEntries(refreshFunctions) } : {},
5439
7145
  viewDependencies: Object.fromEntries(
5440
7146
  (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
5441
7147
  )
@@ -5453,7 +7159,7 @@ function buildViewColumnV2({
5453
7159
  if (!column.nullable) builder = builder.notNull();
5454
7160
  return builder;
5455
7161
  }
5456
- function buildTable2(schema, table, built, enums, byName) {
7162
+ function buildTable2(schema, table, built, enums, byName, introspection) {
5457
7163
  const primaryKeyColumns2 = table.primaryKey ?? [];
5458
7164
  if (primaryKeyColumns2.length === 0) {
5459
7165
  throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
@@ -5469,13 +7175,26 @@ function buildTable2(schema, table, built, enums, byName) {
5469
7175
  const columns = Object.fromEntries(
5470
7176
  table.columns.map((column) => [
5471
7177
  column.columnName,
5472
- buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
7178
+ buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums, introspection })
5473
7179
  ])
5474
7180
  );
5475
7181
  const drizzleTable = schema.table(table.table, columns, (self) => [
5476
7182
  ...buildCompositePrimaryKey(table, primaryKeyColumns2, self),
5477
7183
  ...buildIndexes2(table, self),
5478
- ...buildForeignKeys2(table, self, built, byName)
7184
+ ...(table.checks ?? []).map((check) => pgCore.check(check.name, drizzleOrm.sql.raw(compileCheckExpression(check.expression, table.columns)))),
7185
+ ...buildForeignKeys2(table, self, built, byName),
7186
+ ...(table.foreignKeys ?? []).map((fk) => {
7187
+ const target = fk.target === table.table ? self : built.get(fk.target);
7188
+ if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
7189
+ let constraint = pgCore.foreignKey({
7190
+ name: fk.name,
7191
+ columns: fk.columns.map((column) => self[column]),
7192
+ foreignColumns: fk.references.map((column) => target[column])
7193
+ });
7194
+ if (fk.onDelete !== void 0) constraint = constraint.onDelete(fk.onDelete);
7195
+ if (fk.onUpdate !== void 0) constraint = constraint.onUpdate(fk.onUpdate);
7196
+ return constraint;
7197
+ })
5479
7198
  ]);
5480
7199
  return drizzleTable.enableRLS();
5481
7200
  }
@@ -5492,7 +7211,8 @@ function buildColumnV2({
5492
7211
  column,
5493
7212
  singlePrimaryKey,
5494
7213
  compositeMembers,
5495
- enums
7214
+ enums,
7215
+ introspection
5496
7216
  }) {
5497
7217
  const factory = resolveColumnFactory(table, column, enums);
5498
7218
  let builder = factory(column.columnName);
@@ -5513,7 +7233,7 @@ function buildColumnV2({
5513
7233
  }
5514
7234
  builder = builder.default(drizzleOrm.sql.raw(column.defaultFn));
5515
7235
  } else if (column.default !== void 0 && column.default !== null) {
5516
- builder = builder.default(column.default);
7236
+ builder = introspection && column.type === "jsonb" && typeof column.default === "string" ? builder.default(drizzleOrm.sql.raw(`${quoteLiteral(column.default)}::jsonb`)) : builder.default(column.default);
5517
7237
  }
5518
7238
  return builder;
5519
7239
  }
@@ -5564,11 +7284,14 @@ function buildForeignKeys2(table, self, built, byName) {
5564
7284
  `enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
5565
7285
  );
5566
7286
  }
5567
- return pgCore.foreignKey({
7287
+ let constraint = pgCore.foreignKey({
5568
7288
  name: foreignKeyName(table.table, link.name),
5569
7289
  columns: [sourceColumn],
5570
7290
  foreignColumns: [targetColumn]
5571
7291
  });
7292
+ if (link.onDelete !== void 0) constraint = constraint.onDelete(link.onDelete);
7293
+ if (link.onUpdate !== void 0) constraint = constraint.onUpdate(link.onUpdate);
7294
+ return constraint;
5572
7295
  });
5573
7296
  }
5574
7297
  var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
@@ -5689,16 +7412,31 @@ function findTable2(snapshot, schema, table) {
5689
7412
  const tables = snapshot.tables ?? {};
5690
7413
  return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
5691
7414
  }
5692
- async function planMigrationV2({
7415
+ function planMigrationV2(input) {
7416
+ return planWithViewRecreation({ ...input, viewRecreation: "affected" });
7417
+ }
7418
+ async function planCommittedMigrationV2({
5693
7419
  canonical,
5694
- previousSnapshot
7420
+ previousSnapshot,
7421
+ reproduces
7422
+ }) {
7423
+ const current = await planMigrationV2({ canonical, previousSnapshot });
7424
+ if (reproduces(current)) return { plan: current, reproduced: true };
7425
+ const legacy = await planWithViewRecreation({ canonical, previousSnapshot, viewRecreation: "all-on-base-change" });
7426
+ return reproduces(legacy) ? { plan: legacy, reproduced: true } : { plan: current, reproduced: false };
7427
+ }
7428
+ async function planWithViewRecreation({
7429
+ canonical,
7430
+ previousSnapshot,
7431
+ viewRecreation
5695
7432
  }) {
5696
7433
  const schema = ormSchemaName(canonical.namespace);
5697
7434
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
5698
7435
  assertValidViewDependencies(canonical);
5699
7436
  const hints = applyMigrationHintsV2(previous, canonical);
5700
7437
  const snapshot = generateSnapshotV2(canonical, previousId(previous));
5701
- const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
7438
+ const generatedBaseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
7439
+ const baseDdl = orderForeignKeyAdditions(canonical, generatedBaseDdl);
5702
7440
  const previousViews = snapshotViews(hints.adjusted);
5703
7441
  const currentViews = snapshotViews(snapshot);
5704
7442
  const previousDependencies = snapshotViewDependencies(hints.adjusted);
@@ -5708,7 +7446,14 @@ async function planMigrationV2({
5708
7446
  currentViews,
5709
7447
  previousDependencies,
5710
7448
  currentDependencies,
5711
- baseChanged: hints.statements.length > 0 || baseDdl.length > 0
7449
+ reach: viewReach({
7450
+ viewRecreation,
7451
+ hintStatements: hints.statements,
7452
+ baseDdl,
7453
+ previous,
7454
+ adjusted: hints.adjusted,
7455
+ current: snapshot
7456
+ })
5712
7457
  });
5713
7458
  const dropOrder = topologicalViewNames(
5714
7459
  new Set([...affected].filter((name) => previousViews.has(name))),
@@ -5735,15 +7480,44 @@ async function planMigrationV2({
5735
7480
  ...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
5736
7481
  ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
5737
7482
  ...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
7483
+ ...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql4) => ({ kind: "ddl", sql: sql4 })),
5738
7484
  ...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
5739
7485
  ...createOrder.map((name) => {
5740
7486
  const view = currentViewDefinitions.get(name);
5741
7487
  if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
5742
7488
  return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
5743
- })
7489
+ }),
7490
+ ...refreshFunctionStatements({
7491
+ namespace: canonical.namespace,
7492
+ previous: previous._sequenceOrm?.refreshFunctions ?? {},
7493
+ current: materializedRefreshFunctions(canonical)
7494
+ }).map((sql4) => ({ kind: "ddl", sql: sql4 }))
5744
7495
  ];
5745
7496
  return { namespace: canonical.namespace, schema, statements, snapshot };
5746
7497
  }
7498
+ function orderForeignKeyAdditions(canonical, statements) {
7499
+ if (!canonical.tables.some((table) => table.foreignKeys?.length)) return [...statements];
7500
+ const addsForeignKey = (statement) => /^ALTER TABLE[\s\S]* ADD CONSTRAINT[\s\S]* FOREIGN KEY\b/.test(statement);
7501
+ return [...statements.filter((statement) => !addsForeignKey(statement)), ...statements.filter(addsForeignKey)];
7502
+ }
7503
+ function foreignKeyTimingStatements(canonical, previous, baseDdl) {
7504
+ const prior = previous._sequenceOrm?.foreignKeys ?? {};
7505
+ const result = [];
7506
+ for (const table of canonical.tables) {
7507
+ for (const fk of table.foreignKeys ?? []) {
7508
+ const before = prior[`${table.table}.${fk.name}`];
7509
+ const recreated = baseDdl.some((statement) => statement.includes(`ALTER TABLE ${quoteIdentifier(ormSchemaName(canonical.namespace))}.${quoteIdentifier(table.table)} ADD CONSTRAINT ${quoteIdentifier(fk.name)}`));
7510
+ if (fk.deferrable ? !before || before.deferrable !== fk.deferrable || recreated : before?.deferrable && !recreated) {
7511
+ result.push(foreignKeyTimingSql(ormSchemaName(canonical.namespace), table.table, fk.name, fk.deferrable));
7512
+ }
7513
+ }
7514
+ }
7515
+ return result;
7516
+ }
7517
+ function foreignKeyTimingSql(schema, table, name, timing) {
7518
+ const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
7519
+ return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name)} ${clause}`;
7520
+ }
5747
7521
  function validateViewCatalogStatement({
5748
7522
  canonical,
5749
7523
  view,
@@ -5871,20 +7645,102 @@ function withoutViews(snapshot) {
5871
7645
  delete clone._sequenceOrm;
5872
7646
  return clone;
5873
7647
  }
7648
+ var DROPS_TYPE = /^\s*DROP\s+TYPE\b/i;
7649
+ function viewReach({
7650
+ viewRecreation,
7651
+ hintStatements,
7652
+ baseDdl,
7653
+ previous,
7654
+ adjusted,
7655
+ current
7656
+ }) {
7657
+ if (hintStatements.length === 0 && baseDdl.length === 0) return /* @__PURE__ */ new Set();
7658
+ if (viewRecreation === "all-on-base-change") return "all";
7659
+ if (baseDdl.some((statement) => DROPS_TYPE.test(statement))) return "all";
7660
+ const dependencies = snapshotViewDependencies(adjusted);
7661
+ if ([...snapshotViews(adjusted).keys()].some((name) => dependencies[name] === void 0)) return "all";
7662
+ return baseChangesReachingViews({ previous, adjusted, current });
7663
+ }
7664
+ var SNAPSHOT_BOOKKEEPING = /* @__PURE__ */ new Set(["id", "prevId", "version", "dialect", "_meta", "views", "_sequenceOrm", "enums"]);
7665
+ var VIEW_SAFE_TABLE_FIELDS = /* @__PURE__ */ new Set([
7666
+ "indexes",
7667
+ "foreignKeys",
7668
+ "uniqueConstraints",
7669
+ "checkConstraints",
7670
+ "policies",
7671
+ "isRLSEnabled"
7672
+ ]);
7673
+ var VIEW_SAFE_COLUMN_FIELDS = /* @__PURE__ */ new Set(["notNull", "default", "isUnique", "uniqueName", "nullsNotDistinct"]);
7674
+ function baseChangesReachingViews({
7675
+ previous,
7676
+ adjusted,
7677
+ current
7678
+ }) {
7679
+ const before = adjusted;
7680
+ const after = current;
7681
+ for (const section of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
7682
+ if (SNAPSHOT_BOOKKEEPING.has(section) || section === "tables") continue;
7683
+ if (stableJson(before[section]) !== stableJson(after[section])) return "all";
7684
+ }
7685
+ const relations = /* @__PURE__ */ new Set();
7686
+ const previousTables = snapshotTables(previous);
7687
+ const adjustedTables = snapshotTables(adjusted);
7688
+ for (const key of changedEntryKeys(previousTables, adjustedTables)) {
7689
+ const table = previousTables[key] ?? adjustedTables[key];
7690
+ if (table) relations.add(table.name);
7691
+ }
7692
+ const currentTables = snapshotTables(current);
7693
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(adjustedTables), ...Object.keys(currentTables)])) {
7694
+ const was = adjustedTables[key];
7695
+ const is = currentTables[key];
7696
+ if (!was || !is) {
7697
+ const table = was ?? is;
7698
+ if (table) relations.add(table.name);
7699
+ } else if (tableChangeReachesViews({ was, is })) {
7700
+ relations.add(is.name);
7701
+ }
7702
+ }
7703
+ return relations;
7704
+ }
7705
+ function tableChangeReachesViews({ was, is }) {
7706
+ for (const field of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(is)])) {
7707
+ if (field === "columns" || VIEW_SAFE_TABLE_FIELDS.has(field)) continue;
7708
+ if (stableJson(was[field]) !== stableJson(is[field])) return true;
7709
+ }
7710
+ return Object.entries(was.columns).some(([name, column]) => {
7711
+ const next = is.columns[name];
7712
+ if (!next) return true;
7713
+ return [.../* @__PURE__ */ new Set([...Object.keys(column), ...Object.keys(next)])].some(
7714
+ (field) => !VIEW_SAFE_COLUMN_FIELDS.has(field) && stableJson(column[field]) !== stableJson(next[field])
7715
+ );
7716
+ });
7717
+ }
7718
+ function changedEntryKeys(before, after) {
7719
+ return new Set(
7720
+ [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter(
7721
+ (key) => stableJson(before[key]) !== stableJson(after[key])
7722
+ )
7723
+ );
7724
+ }
7725
+ function snapshotTables(snapshot) {
7726
+ return snapshot.tables ?? {};
7727
+ }
5874
7728
  function affectedViewNames({
5875
7729
  previousViews,
5876
7730
  currentViews,
5877
7731
  previousDependencies,
5878
7732
  currentDependencies,
5879
- baseChanged
7733
+ reach
5880
7734
  }) {
5881
7735
  const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
5882
- if (baseChanged) return allNames;
7736
+ if (reach === "all") return allNames;
7737
+ const dependenciesOf = (name) => /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
5883
7738
  const affected = new Set(
5884
7739
  [...allNames].filter((name) => {
5885
7740
  const before = previousViews.get(name);
5886
7741
  const after = currentViews.get(name);
5887
- return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
7742
+ if (before === void 0 || after === void 0 || stableJson(before) !== stableJson(after)) return true;
7743
+ return [...dependenciesOf(name)].some((dependency) => reach.has(dependency));
5888
7744
  })
5889
7745
  );
5890
7746
  let grew = true;
@@ -5892,8 +7748,7 @@ function affectedViewNames({
5892
7748
  grew = false;
5893
7749
  for (const name of allNames) {
5894
7750
  if (affected.has(name)) continue;
5895
- const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
5896
- if ([...dependencies].some((dependency) => affected.has(dependency))) {
7751
+ if ([...dependenciesOf(name)].some((dependency) => affected.has(dependency))) {
5897
7752
  affected.add(name);
5898
7753
  grew = true;
5899
7754
  }
@@ -6808,7 +8663,9 @@ var PLATFORM_USER_FIELDS = [
6808
8663
  ];
6809
8664
  function generateSdl(canonical, options = {}) {
6810
8665
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
6811
- const tables = canonicalRelations(def).filter((table) => table.source === void 0);
8666
+ const exposed = canonicalRelations(def).filter((table) => table.source === void 0 && table.internal !== true);
8667
+ const cellsByTable = new Map(exposed.map((table) => [table.table, trackedCells(table)]));
8668
+ const tables = exposed.map((table) => ({ ...table, columns: table.columns.filter((column) => column.cell === void 0) }));
6812
8669
  if (tables.length === 0) {
6813
8670
  throw new SdlGenerationError(
6814
8671
  `Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
@@ -6864,8 +8721,14 @@ function generateSdl(canonical, options = {}) {
6864
8721
  '"""Gates an operation behind a platform capability or custom role \u2014 checked by the data plane before execution."""\ndirective @requires(capability: String, role: String) on QUERY | MUTATION'
6865
8722
  );
6866
8723
  blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
8724
+ blocks.push(
8725
+ '"""Abort the entire mutation unless this root affects exactly the document-authored number of rows. Mutation roots only; cannot be conditional."""\ndirective @assertAffectedRows(exactly: Int!) on FIELD'
8726
+ );
8727
+ if (tables.some((table) => table.resolved !== void 0)) blocks.push(...RESOLVED_DIRECTIVE_BLOCKS);
6867
8728
  for (const scalar2 of CUSTOM_SCALARS) blocks.push(`scalar ${scalar2}`);
8729
+ blocks.push(...renderSharedCellTypes([...cellsByTable.values()]));
6868
8730
  blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
8731
+ blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
6869
8732
  for (const enumName of [...usedEnums].sort()) {
6870
8733
  const entry = enumByName.get(enumName);
6871
8734
  if (!entry) continue;
@@ -6888,16 +8751,32 @@ function generateSdl(canonical, options = {}) {
6888
8751
  names,
6889
8752
  namesByTable,
6890
8753
  emitAggregateBoolExp: aggregateBoolExpTargets.has(table.table),
6891
- pinned: options.pinnedColumns?.[table.table] ?? []
8754
+ pinned: options.pinnedColumns?.[table.table] ?? [],
8755
+ cells: cellsByTable.get(table.table) ?? null
6892
8756
  })
6893
8757
  );
6894
8758
  }
6895
8759
  blocks.push(renderQueryRoot(tables, namesByTable));
6896
8760
  const mutationRoot = renderMutationRoot(tables, namesByTable);
6897
8761
  if (mutationRoot !== null) blocks.push(mutationRoot);
8762
+ assertNoDuplicateTypeNames(blocks);
6898
8763
  return `${blocks.join("\n\n")}
6899
8764
  `;
6900
8765
  }
8766
+ var TYPE_DECLARATION_RE = /^(?:type|input|enum|scalar|interface|union)\s+([_A-Za-z][_0-9A-Za-z]*)/;
8767
+ function assertNoDuplicateTypeNames(blocks) {
8768
+ const seen = /* @__PURE__ */ new Set();
8769
+ for (const block of blocks) {
8770
+ const declared = TYPE_DECLARATION_RE.exec(block)?.[1];
8771
+ if (declared === void 0) continue;
8772
+ if (seen.has(declared)) {
8773
+ throw new SdlGenerationError(
8774
+ `Generated schema declares "${declared}" twice. A canonical enum or table name is PascalCased into this GraphQL type name and collides with another declaration (for example an enum named "date_bucket" renders as "DateBucket", the shared bucket enum). Rename the offending enum or table.`
8775
+ );
8776
+ }
8777
+ seen.add(declared);
8778
+ }
8779
+ }
6901
8780
  function assertEnumValue(value, context) {
6902
8781
  if (!GRAPHQL_NAME_RE2.test(value) || RESERVED_ENUM_VALUES.has(value)) {
6903
8782
  throw new SdlGenerationError(
@@ -6963,6 +8842,52 @@ ${orderFields}
6963
8842
  }`
6964
8843
  ];
6965
8844
  }
8845
+ var RESOLVED_DIRECTIVE_BLOCKS = [
8846
+ '"""Refresh this resolved-table root from a prefetch source before reading. Key-addressed roots only; the source must support the root shape."""\ndirective @prefetch(source: String!, maxStaleness: String) repeatable on FIELD',
8847
+ renderEnum("WritebackMode", ["SYNC", "ASYNC"]),
8848
+ '"""Write this resolved-table mutation back to a prefetch source (SYNC rolls back on failure; ASYNC queues)."""\ndirective @writeback(source: String!, mode: WritebackMode) repeatable on FIELD',
8849
+ '"""Declare that this mutation deliberately keeps writeback columns local to the named source."""\ndirective @localOnly(source: String!) repeatable on FIELD'
8850
+ ];
8851
+ function trackedCells(table) {
8852
+ const columns = [...new Set(table.columns.flatMap((column) => column.cell ? [column.cell.of] : []))];
8853
+ if (columns.length === 0) return null;
8854
+ return { resolved: table.columns.some((column) => column.cell?.part === "source"), columns };
8855
+ }
8856
+ function renderSharedCellTypes(all) {
8857
+ const present = all.filter((cells) => cells !== null);
8858
+ const blocks = [];
8859
+ if (present.some((cells) => !cells.resolved)) {
8860
+ blocks.push(`"""When, and by whom, a cell was last written."""
8861
+ type ${CELL_TYPENAMES.stamp} {
8862
+ at: timestamptz!
8863
+ by: String
8864
+ }`);
8865
+ }
8866
+ if (present.some((cells) => cells.resolved)) {
8867
+ blocks.push(`"""A newer candidate value that lost to the column's resolve rule."""
8868
+ type ${CELL_TYPENAMES.conflict} {
8869
+ source: String!
8870
+ at: timestamptz
8871
+ value: jsonb
8872
+ }`);
8873
+ blocks.push(`"""How a resolved cell was decided: the winning source, its time and actor, and newer losers."""
8874
+ type ${CELL_TYPENAMES.resolved} {
8875
+ source: String!
8876
+ at: timestamptz
8877
+ by: String
8878
+ conflicts: [${CELL_TYPENAMES.conflict}!]!
8879
+ }`);
8880
+ }
8881
+ return blocks;
8882
+ }
8883
+ function renderCellsType(ctx) {
8884
+ if (!ctx.cells) return [];
8885
+ const cellType = ctx.cells.resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp;
8886
+ const lines = ctx.cells.columns.map((column) => ` ${fieldName(column)}: ${cellType}`);
8887
+ return [`type ${cellsTypename(ctx.names.typename)} {
8888
+ ${lines.join("\n")}
8889
+ }`];
8890
+ }
6966
8891
  function relationArgs(target) {
6967
8892
  return `where: ${target.boolExp}, orderBy: [${target.orderBy}!], limit: Int, offset: Int`;
6968
8893
  }
@@ -6971,10 +8896,10 @@ function rootArgs(names) {
6971
8896
  }
6972
8897
  function primaryKeyColumns(table) {
6973
8898
  if (!table.primaryKey || table.primaryKey.length === 0) return [];
6974
- return table.primaryKey.map((identifier2) => {
6975
- const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
8899
+ return table.primaryKey.map((identifier3) => {
8900
+ const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
6976
8901
  if (!column) {
6977
- throw new SdlGenerationError(`Primary key column "${identifier2}" not found on table "${table.table}".`);
8902
+ throw new SdlGenerationError(`Primary key column "${identifier3}" not found on table "${table.table}".`);
6978
8903
  }
6979
8904
  return column;
6980
8905
  });
@@ -6983,10 +8908,12 @@ function renderTableBlocks(ctx) {
6983
8908
  const { table, names } = ctx;
6984
8909
  const blocks = [
6985
8910
  renderObjectType(ctx),
8911
+ ...renderCellsType(ctx),
6986
8912
  renderSelectColumnEnum(ctx),
6987
8913
  renderOrderByInput(ctx),
6988
8914
  renderBoolExp(ctx),
6989
- ...renderAggregateTypes(ctx)
8915
+ ...renderAggregateTypes(ctx),
8916
+ ...renderGroupByTypes(ctx)
6990
8917
  ];
6991
8918
  if (ctx.emitAggregateBoolExp) blocks.push(...renderAggregateBoolExp(ctx));
6992
8919
  const pk = primaryKeyColumns(table);
@@ -6996,11 +8923,49 @@ function renderTableBlocks(ctx) {
6996
8923
  ${fields}
6997
8924
  }`);
6998
8925
  }
6999
- if (allowsInsert(table) || allowsUpdate(table)) {
8926
+ if (ctx.table.resolved !== void 0) {
8927
+ blocks.push(...renderResolvedWriteBlocks(ctx));
8928
+ } else if (allowsInsert(table) || allowsUpdate(table)) {
7000
8929
  blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
7001
8930
  }
7002
8931
  return blocks;
7003
8932
  }
8933
+ function resolvedWritableColumns(table) {
8934
+ const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
8935
+ return table.columns.filter((column) => !system.has(column.name));
8936
+ }
8937
+ function renderResolvedWriteBlocks(ctx) {
8938
+ const { table, names } = ctx;
8939
+ const writable = resolvedWritableColumns(table);
8940
+ const pk = new Set(table.primaryKey ?? []);
8941
+ const settable = writable.filter((column) => !pk.has(column.columnName));
8942
+ const line = (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`;
8943
+ const blocks = [];
8944
+ if (allowsInsert(table)) blocks.push(`input ${names.insertInput} {
8945
+ ${writable.map(line).join("\n")}
8946
+ }`);
8947
+ if (allowsUpdate(table)) {
8948
+ blocks.push(`input ${names.setInput} {
8949
+ ${settable.map(line).join("\n")}
8950
+ }`);
8951
+ blocks.push(renderEnum(names.updateColumnEnum, settable.map((column) => fieldName(column.name))));
8952
+ }
8953
+ return blocks;
8954
+ }
8955
+ function renderResolvedMutationFields(table, names) {
8956
+ const lines = [];
8957
+ const pk = primaryKeyColumns(table);
8958
+ if (allowsInsert(table)) lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!): ${names.typename}`);
8959
+ if (allowsUpdate(table) && pk.length > 0) {
8960
+ lines.push(` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, _set: ${names.setInput}!): ${names.typename}`);
8961
+ lines.push(` ${names.revertByPkRoot}(pkColumns: ${names.pkColumnsInput}!, columns: [${names.updateColumnEnum}!]!): ${names.typename}`);
8962
+ if ((table.resolved?.deletedBy.length ?? 0) > 0) {
8963
+ const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
8964
+ lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
8965
+ }
8966
+ }
8967
+ return lines;
8968
+ }
7004
8969
  function allowsInsert(table) {
7005
8970
  return table.policies.write !== void 0 || table.policies.insert !== void 0;
7006
8971
  }
@@ -7023,6 +8988,10 @@ function renderObjectType(ctx) {
7023
8988
  const deprecated = column.deprecated ? " @deprecated" : "";
7024
8989
  lines.push(` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}${bang}${deprecated}`);
7025
8990
  }
8991
+ if (ctx.cells) {
8992
+ lines.push(' """Per-cell metadata: when each field was last written, and by whom."""');
8993
+ lines.push(` ${CELLS_FIELD}: ${cellsTypename(names.typename)}!`);
8994
+ }
7026
8995
  for (const link of table.links) {
7027
8996
  if (link.description !== void 0) lines.push(...descriptionLines(link.description, " "));
7028
8997
  if (link.kind === "toPlatformUser") {
@@ -7091,7 +9060,10 @@ function numericishColumns(table) {
7091
9060
  return table.columns.filter((column) => typeof column.type !== "object" && NUMERICISH.has(column.type));
7092
9061
  }
7093
9062
  function jsonbColumns(table) {
7094
- return table.columns.filter((column) => column.type === "jsonb");
9063
+ return clientWritableColumns(table).filter((column) => column.type === "jsonb");
9064
+ }
9065
+ function clientWritableColumns(table) {
9066
+ return table.columns;
7095
9067
  }
7096
9068
  function renderAggregateTypes(ctx) {
7097
9069
  const { table, names } = ctx;
@@ -7123,6 +9095,32 @@ ${sumLines.join("\n")}
7123
9095
  }
7124
9096
  return blocks;
7125
9097
  }
9098
+ function groupableColumns(table) {
9099
+ return table.columns.filter((column) => column.type !== "jsonb");
9100
+ }
9101
+ function renderGroupByTypes(ctx) {
9102
+ const { table, names } = ctx;
9103
+ const groupable = groupableColumns(table);
9104
+ if (groupable.length === 0) return [];
9105
+ const keyLines = groupable.map((column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`);
9106
+ return [
9107
+ renderEnum(names.groupByColumnEnum, groupable.map((column) => fieldName(column.name))),
9108
+ `input ${names.groupByInput} {
9109
+ column: ${names.groupByColumnEnum}!
9110
+ bucket: ${DATE_BUCKET_ENUM}
9111
+ }`,
9112
+ `type ${names.groupKey} {
9113
+ ${keyLines.join("\n")}
9114
+ }`,
9115
+ `type ${names.aggregateGroup} {
9116
+ key: ${names.groupKey}!
9117
+ aggregate: ${names.aggregateFields}
9118
+ }`
9119
+ ];
9120
+ }
9121
+ function groupRootArgs(names) {
9122
+ return `where: ${names.boolExp}, groupBy: [${names.groupByInput}!]!, limit: Int, offset: Int`;
9123
+ }
7126
9124
  function renderAggregateBoolExp(ctx) {
7127
9125
  const { names } = ctx;
7128
9126
  return [
@@ -7148,12 +9146,15 @@ function constraintValues(table) {
7148
9146
  }
7149
9147
  return [...values].sort();
7150
9148
  }
9149
+ function clientSettableColumns(table) {
9150
+ return clientWritableColumns(table).filter((column) => column.onUpdateFn === void 0);
9151
+ }
7151
9152
  function renderWriteBlocks(ctx, access) {
7152
9153
  const { table, names, pinned } = ctx;
7153
9154
  const blocks = [];
7154
9155
  if (access.insert) {
7155
9156
  const pinnedSet = new Set(pinned);
7156
- const insertable = table.columns.filter(
9157
+ const insertable = clientWritableColumns(table).filter(
7157
9158
  (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
7158
9159
  );
7159
9160
  if (insertable.length === 0) {
@@ -7169,7 +9170,7 @@ ${insertLines.join("\n")}
7169
9170
  }`);
7170
9171
  }
7171
9172
  if (access.update) {
7172
- const setLines = table.columns.map(
9173
+ const setLines = clientSettableColumns(table).map(
7173
9174
  (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
7174
9175
  );
7175
9176
  blocks.push(`input ${names.setInput} {
@@ -7196,7 +9197,7 @@ ${incLines.join("\n")}
7196
9197
  if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
7197
9198
  for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
7198
9199
  blocks.push(renderEnum(names.constraintEnum, constraints));
7199
- const updateColumns = table.columns.map((column) => fieldName(column.name));
9200
+ const updateColumns = clientSettableColumns(table).map((column) => fieldName(column.name));
7200
9201
  blocks.push(renderEnum(names.updateColumnEnum, updateColumns));
7201
9202
  blocks.push(
7202
9203
  `input ${names.onConflict} {
@@ -7228,6 +9229,9 @@ function renderQueryRoot(tables, namesByTable) {
7228
9229
  lines.push(` ${names.byPkRoot}(${args}): ${names.typename}`);
7229
9230
  }
7230
9231
  lines.push(` ${names.aggregateRoot}(${rootArgs(names)}): ${names.aggregate}!`);
9232
+ if (groupableColumns(table).length > 0) {
9233
+ lines.push(` ${names.aggregateGroupsRoot}(${groupRootArgs(names)}): [${names.aggregateGroup}!]!`);
9234
+ }
7231
9235
  }
7232
9236
  lines.push("}");
7233
9237
  return lines.join("\n");
@@ -7243,6 +9247,10 @@ function renderMutationRoot(tables, namesByTable) {
7243
9247
  if (!canInsert && !canUpdate) continue;
7244
9248
  const names = namesByTable.get(table.table);
7245
9249
  if (!names) continue;
9250
+ if (table.resolved !== void 0) {
9251
+ lines.push(...renderResolvedMutationFields(table, names));
9252
+ continue;
9253
+ }
7246
9254
  const constraints = constraintValues(table);
7247
9255
  const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
7248
9256
  const updateArgs = [`_set: ${names.setInput}`];
@@ -7296,6 +9304,92 @@ function renderCowMutationFields(table, names) {
7296
9304
  }
7297
9305
  return lines;
7298
9306
  }
9307
+ var ORM_MIGRATION_EXPORT_FILE = "orm.migration.json";
9308
+ var ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
9309
+ var hash = zod.z.string().regex(/^sha256:[0-9a-f]{64}$/);
9310
+ var ormMigrationPayloadSchema = zod.z.object({
9311
+ canonical_definition: canonicalDefinitionSchema,
9312
+ documents: zod.z.array(zod.z.object({ path: zod.z.string().min(1).max(2048), contents: zod.z.string().max(1e6) }).strict()).max(5e3),
9313
+ client_manifest: zod.z.object({ ops_hash: hash, operations: zod.z.record(zod.z.string(), hash) }).strict(),
9314
+ sdl: zod.z.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_GENERATED_SDL_BYTES, {
9315
+ message: `sdl exceeds ${MAX_GENERATED_SDL_BYTES} bytes`
9316
+ }),
9317
+ schema_snapshot: zod.z.json(),
9318
+ // The existing ORM committed-migration validator and replay engine remain
9319
+ // authoritative. This envelope hashes the provenance, never authorizes SQL.
9320
+ migration: zod.z.json().optional()
9321
+ }).strict();
9322
+ zod.z.object({
9323
+ schema_version: zod.z.literal(1),
9324
+ schema_hash: hash,
9325
+ payload: ormMigrationPayloadSchema,
9326
+ checksum: zod.z.string().regex(/^[0-9a-f]{64}$/)
9327
+ }).strict();
9328
+ function checksum(value) {
9329
+ return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
9330
+ }
9331
+ function createOrmMigrationExport({ schemaHash, payload }) {
9332
+ const contents = { schema_version: 1, schema_hash: hash.parse(schemaHash), payload: ormMigrationPayloadSchema.parse(payload) };
9333
+ return { ...contents, checksum: checksum(contents) };
9334
+ }
9335
+ function renderOrmMigrationExport(exported) {
9336
+ const text4 = `${JSON.stringify(exported, null, 2)}
9337
+ `;
9338
+ if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
9339
+ return text4;
9340
+ }
9341
+
9342
+ // src/compiler-gql/role-warnings.ts
9343
+ var ROLE_CHECK = /\brole\(\s*['"]([^'"]+)['"]\s*\)/g;
9344
+ function isRecord(value) {
9345
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9346
+ }
9347
+ function roleKeysIn(value) {
9348
+ const keys = /* @__PURE__ */ new Set();
9349
+ for (const match of JSON.stringify(value ?? null).matchAll(ROLE_CHECK)) {
9350
+ if (match[1]) keys.add(match[1]);
9351
+ }
9352
+ return [...keys].sort();
9353
+ }
9354
+ function roleWarnings({
9355
+ canonical,
9356
+ manifest
9357
+ }) {
9358
+ const warnings = [];
9359
+ const roles = canonical.capabilities.roles;
9360
+ const defined = isRecord(roles) && isRecord(roles.definitions) ? Object.keys(roles.definitions).sort() : [];
9361
+ if (defined.length > 0) {
9362
+ warnings.push({
9363
+ severity: "warning",
9364
+ code: "deprecated-role-definitions",
9365
+ message: `The namespace defines roles (${defined.join(", ")}). Role definitions here are a deprecated one-time seed: apply creates any that don't exist yet and never updates them after. Create and edit roles in Atlas; keep declaring the capabilities your policies check.`
9366
+ });
9367
+ }
9368
+ const relations = [
9369
+ ...canonical.tables,
9370
+ ...(canonical.views ?? []).map((view) => ({ table: view.view, policies: view.policies, resolved: view.resolved }))
9371
+ ];
9372
+ for (const relation of relations) {
9373
+ const keys = roleKeysIn([relation.policies, relation.resolved]);
9374
+ if (keys.length === 0) continue;
9375
+ warnings.push({
9376
+ severity: "warning",
9377
+ code: "role-check",
9378
+ table: relation.table,
9379
+ message: `Policy checks ${keys.map((key) => `role('${key}')`).join(", ")}. Roles are defined in Atlas and can be renamed or removed there; check the capability the role grants with capability('\u2026') instead.`
9380
+ });
9381
+ }
9382
+ for (const [operation, entry] of Object.entries(manifest.operations).sort(([a], [b]) => a.localeCompare(b))) {
9383
+ if (!entry.requires?.role) continue;
9384
+ warnings.push({
9385
+ severity: "warning",
9386
+ code: "role-requires",
9387
+ operation,
9388
+ message: `@requires(role: "${entry.requires.role}") gates on a role Atlas can rename or remove; use @requires(capability: "\u2026") with the capability the role grants.`
9389
+ });
9390
+ }
9391
+ return warnings;
9392
+ }
7299
9393
 
7300
9394
  // src/cli.ts
7301
9395
  var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
@@ -7303,7 +9397,9 @@ var TOKEN_ENV = "SEQUENCE_ORM_TOKEN";
7303
9397
  var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
7304
9398
  var USAGE = `usage:
7305
9399
  seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
7306
- seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (+ consumer graphql-codegen when codegen.ts is present)
9400
+ seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (+ sources.gen.ts for resolved-table sources, + consumer graphql-codegen when codegen.ts is present)
9401
+ seq-studio orm generate [dir] --migration-export [--check]
9402
+ export reviewed ORM inputs for a coordinated pipeline migration; --check verifies without writing
7307
9403
  seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
7308
9404
  seq-studio orm plan [dir] -e <env> offline dry run: compile definitions and diff against the registry (no database, no writes)
7309
9405
  seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh the generated outputs (the everyday command)
@@ -7318,6 +9414,8 @@ var USAGE = `usage:
7318
9414
  v2 = sequence.config.ts + schema/*.ts + graphql/**; v1 (legacy) = one <table>.yaml per table.
7319
9415
  Environments: see \`seq-studio envs list\` (discovered after you authenticate).
7320
9416
  Authenticate with: seq-studio login
9417
+ Execute endpoint rollout: SEQUENCE_ORM_ATLAS_EXECUTE_V2=1 opts into /api/data/v2;
9418
+ otherwise execute uses /_data/v2. Server rollout and authorization still apply.
7321
9419
  `;
7322
9420
  var tokenProvider = null;
7323
9421
  function setTokenProvider(provider) {
@@ -7470,6 +9568,7 @@ async function walkFiles(root, prefix = "") {
7470
9568
  var SCHEMA_GRAPHQL_FILE = "schema.graphql";
7471
9569
  var OPERATIONS_MANIFEST_FILE = "operations.manifest.json";
7472
9570
  var TYPE_POLICIES_FILE = "typePolicies.gen.ts";
9571
+ var SOURCE_CONTRACTS_FILE = "sources.gen.ts";
7473
9572
  var GRAPHQL_DIRNAME = "graphql";
7474
9573
  var CODEGEN_CONFIG_FILE = "codegen.ts";
7475
9574
  function ormLoadOptions() {
@@ -7489,9 +9588,10 @@ async function compileV2Project(dir) {
7489
9588
  const documents = await collectGraphqlDocuments(dir);
7490
9589
  const result = compileNamespace({ canonical: loaded.canonical, sdl, documents });
7491
9590
  if (!result.ok) return { ok: false, diagnostics: result.diagnostics };
9591
+ const diagnostics = [...result.diagnostics, ...roleWarnings({ canonical: loaded.canonical, manifest: result.manifest })];
7492
9592
  return {
7493
9593
  ok: true,
7494
- project: { loaded, sdl, documents, manifest: result.manifest, diagnostics: result.diagnostics }
9594
+ project: { loaded, sdl, documents, manifest: result.manifest, diagnostics }
7495
9595
  };
7496
9596
  }
7497
9597
  var GQL_TEMPLATE_RE = /(?:\bgql|\/\*\s*GraphQL\s*\*\/)\s*`([^`]*)`/g;
@@ -7532,6 +9632,12 @@ function printDiagnostics(diagnostics) {
7532
9632
  }
7533
9633
  async function generateCommand(rest) {
7534
9634
  const dir = positional(rest) ?? ".";
9635
+ const migrationExport = rest.includes("--migration-export");
9636
+ const check = rest.includes("--check");
9637
+ if (check && !migrationExport) {
9638
+ console.error("generate --check requires --migration-export");
9639
+ return 1;
9640
+ }
7535
9641
  if (await detectProjectFormat(dir) === "v1") {
7536
9642
  console.error(`generate needs a v2 project (${NAMESPACE_CONFIG_FILE}) \u2014 convert with: seq-studio orm migrate-from-yaml ${dir}`);
7537
9643
  return 1;
@@ -7539,6 +9645,36 @@ async function generateCommand(rest) {
7539
9645
  const compiled = await compileV2Project(dir);
7540
9646
  printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
7541
9647
  if (!compiled.ok) return 1;
9648
+ if (migrationExport) {
9649
+ if (await validateV2Command(dir) !== 0) return 1;
9650
+ const { loaded: loaded2, sdl, documents, manifest: manifest2 } = compiled.project;
9651
+ const workspace = await readMigrationWorkspace(dir, loaded2.canonical.namespace);
9652
+ const migration = await loadCommittedTipMigrationV2({ dir, loaded: loaded2, workspace });
9653
+ const exported = createOrmMigrationExport({ schemaHash: manifest2.schemaHash, payload: {
9654
+ canonical_definition: loaded2.canonical,
9655
+ documents,
9656
+ sdl,
9657
+ client_manifest: { ops_hash: manifest2.opsHash, operations: Object.fromEntries(Object.entries(manifest2.operations).map(([name, operation]) => [name, operation.opHash])) },
9658
+ schema_snapshot: workspace.tipSnapshot,
9659
+ ...migration ? { migration } : {}
9660
+ } });
9661
+ const text4 = renderOrmMigrationExport(exported);
9662
+ const path$1 = path.join(dir, ORM_MIGRATION_EXPORT_FILE);
9663
+ if (check) {
9664
+ const committed = await promises.readFile(path$1, "utf8").catch((error) => {
9665
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
9666
+ throw error;
9667
+ });
9668
+ if (committed !== text4) {
9669
+ console.error(`${ORM_MIGRATION_EXPORT_FILE} is missing or stale; run seq-studio orm generate ${dir} --migration-export and commit it`);
9670
+ return 1;
9671
+ }
9672
+ console.log(`${ORM_MIGRATION_EXPORT_FILE} matches the current definitions, operations, and committed migration`);
9673
+ return 0;
9674
+ }
9675
+ await promises.writeFile(path$1, text4);
9676
+ console.log(`wrote ${ORM_MIGRATION_EXPORT_FILE} (${exported.checksum.slice(0, 12)})`);
9677
+ }
7542
9678
  await writeGenerateOutputs(dir, compiled.project);
7543
9679
  const { loaded, manifest } = compiled.project;
7544
9680
  console.log(
@@ -7559,6 +9695,8 @@ async function writeGenerateOutputs(dir, project) {
7559
9695
  await promises.writeFile(path.join(dir, OPERATIONS_MANIFEST_FILE), `${JSON.stringify(manifestFile, null, 2)}
7560
9696
  `);
7561
9697
  await promises.writeFile(path.join(dir, TYPE_POLICIES_FILE), generateTypePolicies(loaded.canonical));
9698
+ const sourceContracts = generateSourceContracts(loaded.canonical);
9699
+ if (sourceContracts !== null) await promises.writeFile(path.join(dir, SOURCE_CONTRACTS_FILE), sourceContracts);
7562
9700
  }
7563
9701
  async function runConsumerCodegen(dir) {
7564
9702
  const codegenPath = path.join(dir, CODEGEN_CONFIG_FILE);
@@ -7654,9 +9792,10 @@ async function executeV2Command(rest) {
7654
9792
  );
7655
9793
  }
7656
9794
  const variables = await readExecuteVariables(variablesFile);
9795
+ const executePrefix = process.env["SEQUENCE_ORM_ATLAS_EXECUTE_V2"] === "1" ? "/api/data/v2" : "/_data/v2";
7657
9796
  const response = await apiRequestStrict(
7658
9797
  "POST",
7659
- `/_data/v2/${loaded.canonical.namespace}`,
9798
+ `${executePrefix}/${loaded.canonical.namespace}`,
7660
9799
  { op: selected.hash, variables }
7661
9800
  );
7662
9801
  console.log(JSON.stringify(response, null, 2));
@@ -7825,16 +9964,28 @@ async function checkWorkspaceV2({
7825
9964
  return 1;
7826
9965
  }
7827
9966
  const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
7828
- const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
7829
- const regenerated = renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan });
7830
9967
  const committed = await readMigrationSql(dir, tip);
7831
- if (regenerated !== committed && !matchesRebaseTip({ committed, tip, namespace, plan })) {
9968
+ const { reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
9969
+ if (!reproduced) {
7832
9970
  log(`${MIGRATIONS_DIRNAME}/${tip.name}.sql does not regenerate from the definitions \u2014 re-run \`seq-studio orm diff\``);
7833
9971
  return 1;
7834
9972
  }
7835
9973
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
7836
9974
  return 0;
7837
9975
  }
9976
+ function planCommittedTipV2({
9977
+ loaded,
9978
+ tip,
9979
+ previousSnapshot,
9980
+ committed
9981
+ }) {
9982
+ const namespace = loaded.canonical.namespace;
9983
+ return planCommittedMigrationV2({
9984
+ canonical: loaded.canonical,
9985
+ previousSnapshot,
9986
+ reproduces: (plan) => renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan }) === committed || matchesRebaseTip({ committed, tip, namespace, plan })
9987
+ });
9988
+ }
7838
9989
  function matchesRebaseTip({
7839
9990
  committed,
7840
9991
  tip,
@@ -7937,7 +10088,6 @@ async function loadCommittedTipMigrationV2({
7937
10088
  loaded,
7938
10089
  workspace
7939
10090
  }) {
7940
- const namespace = loaded.canonical.namespace;
7941
10091
  const tip = workspace.journal.entries.at(-1);
7942
10092
  if (!tip) return void 0;
7943
10093
  if (tip.content_hash !== loaded.hash) {
@@ -7947,10 +10097,9 @@ async function loadCommittedTipMigrationV2({
7947
10097
  );
7948
10098
  }
7949
10099
  const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
7950
- const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
7951
10100
  const committed = await readMigrationSql(dir, tip);
7952
- const regenerated = renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan });
7953
- if (regenerated !== committed && !matchesRebaseTip({ committed, tip, namespace, plan })) {
10101
+ const { plan, reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
10102
+ if (!reproduced) {
7954
10103
  throw new OrmDefinitionError(
7955
10104
  `${tip.name}.sql does not regenerate from the definitions \u2014 run \`seq-studio orm diff --check\``,
7956
10105
  MIGRATIONS_DIRNAME
@@ -8171,6 +10320,7 @@ function columnProperty(table, column, singlePrimaryKey) {
8171
10320
  }
8172
10321
  chain += `.default(${JSON.stringify(column.default)})`;
8173
10322
  }
10323
+ if (column.onUpdateFn !== void 0) chain += `.onUpdate(${quote(column.onUpdateFn)})`;
8174
10324
  if (column.description !== void 0) chain += `.description(${quote(column.description)})`;
8175
10325
  if (column.renamedFrom !== void 0) chain += `.renamedFrom(${quote(column.renamedFrom)})`;
8176
10326
  if (column.cast !== void 0) chain += `.cast(${quote(column.cast)})`;