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