@sequenceholdings/orm 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +38 -1
  2. package/dist/authoring.cjs +1358 -45
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +223 -2
  5. package/dist/authoring.d.ts +223 -2
  6. package/dist/authoring.js +1349 -47
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +662 -38
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +696 -53
  11. package/dist/canonical.d.ts +696 -53
  12. package/dist/canonical.js +647 -39
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +1808 -247
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +1809 -248
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs +90 -25
  19. package/dist/client.cjs.map +1 -1
  20. package/dist/client.d.cts +39 -22
  21. package/dist/client.d.ts +39 -22
  22. package/dist/client.js +87 -26
  23. package/dist/client.js.map +1 -1
  24. package/dist/compiler.cjs +1325 -285
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +23 -2
  27. package/dist/compiler.d.ts +23 -2
  28. package/dist/compiler.js +1325 -285
  29. package/dist/compiler.js.map +1 -1
  30. package/dist/constants-YoFGjIBM.d.cts +99 -0
  31. package/dist/constants-YoFGjIBM.d.ts +99 -0
  32. package/dist/dynamic-input.d.cts +1 -0
  33. package/dist/dynamic-input.d.ts +1 -0
  34. package/dist/index.cjs +1605 -140
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +230 -113
  37. package/dist/index.d.ts +230 -113
  38. package/dist/index.js +1583 -142
  39. package/dist/index.js.map +1 -1
  40. package/dist/ir.cjs +727 -38
  41. package/dist/ir.cjs.map +1 -1
  42. package/dist/ir.d.cts +539 -21
  43. package/dist/ir.d.ts +539 -21
  44. package/dist/ir.js +722 -39
  45. package/dist/ir.js.map +1 -1
  46. package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
  47. package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
  48. package/dist/policy-1iZbGnZZ.d.cts +24 -0
  49. package/dist/policy-1iZbGnZZ.d.ts +24 -0
  50. package/dist/predicate-v2.cjs +726 -42
  51. package/dist/predicate-v2.cjs.map +1 -1
  52. package/dist/predicate-v2.d.cts +2 -1
  53. package/dist/predicate-v2.d.ts +2 -1
  54. package/dist/predicate-v2.js +726 -42
  55. package/dist/predicate-v2.js.map +1 -1
  56. package/dist/sdl.cjs +858 -54
  57. package/dist/sdl.cjs.map +1 -1
  58. package/dist/sdl.d.cts +7 -3
  59. package/dist/sdl.d.ts +7 -3
  60. package/dist/sdl.js +856 -55
  61. package/dist/sdl.js.map +1 -1
  62. package/package.json +9 -4
  63. package/templates/orm-namespace/CLAUDE.md +6 -4
  64. package/templates/orm-namespace/codegen.preset.d.cts +7 -0
  65. package/templates/orm-namespace/codegen.ts +2 -1
  66. package/templates/orm-namespace/graphql/example/queries.ts +4 -3
  67. package/templates/orm-namespace/package.json +2 -1
  68. package/templates/orm-namespace/schema/tables.ts +3 -2
  69. package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ 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, foreignKey, primaryKey, uniqueIndex, index, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
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
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';
@@ -37,12 +37,217 @@ var PredicateParseError = class extends Error {
37
37
  name = "PredicateParseError";
38
38
  };
39
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
+
40
245
  // src/constants.ts
41
246
  var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
42
247
  var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
43
248
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
44
249
  var MAX_IDENTIFIER_LENGTH = 63;
45
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
250
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
46
251
  var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
47
252
  var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
48
253
  var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
@@ -58,11 +263,291 @@ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
58
263
  var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
59
264
  var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
60
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
+ }
61
546
 
62
547
  // src/canonical/v2.ts
63
548
  var CANONICAL_FORMAT = 2;
64
549
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
65
- var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
550
+ var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
66
551
  var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
67
552
  var COLUMN_TYPES = [
68
553
  "uuid",
@@ -78,14 +563,14 @@ var COLUMN_TYPES = [
78
563
  ];
79
564
  var columnTypeSchema = z.enum(COLUMN_TYPES);
80
565
  var canonicalEnumSchema = z.object({
81
- name: identifier,
566
+ name: identifier2,
82
567
  values: z.array(z.string().min(1).max(63)).min(1).max(100)
83
568
  });
84
569
  var canonicalColumnSchema = z.object({
85
570
  /** Property name in TS (camelCase); columnName is the Postgres name. */
86
571
  name: z.string().min(1).max(63),
87
572
  columnName: columnIdentifier,
88
- type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
573
+ type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
89
574
  nullable: z.boolean().default(true),
90
575
  unique: z.boolean().default(false),
91
576
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -101,7 +586,14 @@ var canonicalColumnSchema = z.object({
101
586
  /** Migration hints — same semantics as v1. */
102
587
  renamedFrom: columnIdentifier.optional(),
103
588
  cast: z.string().max(200).optional(),
104
- 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()
105
597
  }).superRefine((column, ctx) => {
106
598
  if (column.onUpdateFn === void 0) return;
107
599
  if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
@@ -116,36 +608,26 @@ var canonicalLinkSchema = z.object({
116
608
  name: z.string().min(1).max(63),
117
609
  kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
118
610
  /** Target table name (namespace-local). Absent for toPlatformUser. */
119
- target: identifier.optional(),
611
+ target: identifier2.optional(),
120
612
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
121
613
  on: columnIdentifier,
122
614
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
123
615
  references: columnIdentifier.optional(),
124
616
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
125
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(),
126
620
  description: z.string().max(500).optional()
127
621
  });
128
622
  var canonicalIndexSchema = z.object({
129
623
  columns: z.array(columnIdentifier).min(1),
130
624
  unique: z.boolean().default(false),
131
- name: identifier.optional()
625
+ name: identifier2.optional()
132
626
  });
133
- var policyVariantSchema = z.object({
134
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
135
- when: z.string().min(1).max(1e3),
136
- /** Exactly one of: allow-all bypass, or a row predicate. */
137
- allow: z.literal(true).optional(),
138
- policy: z.string().min(1).max(2e3).optional()
139
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
140
- message: "A variant declares exactly one of allow: true or policy"
141
- });
142
- var policyExpressionSchema = z.union([
143
- z.string().min(1).max(2e3),
144
- z.object({
145
- default: z.string().min(1).max(2e3),
146
- variants: z.array(policyVariantSchema).max(10).default([])
147
- })
148
- ]);
627
+ var canonicalCheckSchema = z.object({
628
+ name: identifier2,
629
+ expression: z.string().min(1).max(2e3)
630
+ }).strict();
149
631
  var canonicalPoliciesSchema = z.object({
150
632
  read: policyExpressionSchema.optional(),
151
633
  write: policyExpressionSchema.optional(),
@@ -333,7 +815,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
333
815
  "width_bucket"
334
816
  ];
335
817
  var canonicalActionSchema = z.object({
336
- name: identifier,
818
+ name: identifier2,
337
819
  requires: z.string().max(1e3).optional(),
338
820
  params: z.record(z.string(), z.object({
339
821
  type: columnTypeSchema,
@@ -345,9 +827,9 @@ var canonicalActionSchema = z.object({
345
827
  });
346
828
  var canonicalCowMutationSchema = z.object({
347
829
  kind: z.enum(["override", "revert"]),
348
- name: identifier,
830
+ name: identifier2,
349
831
  /** Synced table (same namespace) the overlay copies from / falls back to. */
350
- synced: identifier,
832
+ synced: identifier2,
351
833
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
352
834
  pk: z.string().min(1).max(63),
353
835
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -375,23 +857,27 @@ var canonicalCowMutationSchema = z.object({
375
857
  }
376
858
  });
377
859
  var canonicalForeignKeySchema = z.object({
378
- name: identifier,
860
+ name: identifier2,
379
861
  columns: z.array(columnIdentifier).min(1).max(8),
380
- target: identifier,
862
+ target: identifier2,
381
863
  references: z.array(columnIdentifier).min(1).max(8),
382
- deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional()
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()
383
867
  }).strict();
384
868
  var canonicalTableSchema = z.object({
385
- table: identifier,
869
+ table: identifier2,
386
870
  description: z.string().max(2e3).optional(),
387
871
  /** Table-level rename hint (new in v2). */
388
- renamedFrom: identifier.optional(),
872
+ renamedFrom: identifier2.optional(),
389
873
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
390
874
  source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
391
875
  columns: z.array(canonicalColumnSchema).default([]),
392
876
  primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
393
877
  links: z.array(canonicalLinkSchema).default([]),
394
878
  indexes: z.array(canonicalIndexSchema).default([]),
879
+ /** Omitted by old definitions, preserving their existing content hashes. */
880
+ checks: z.array(canonicalCheckSchema).max(100).optional(),
395
881
  policies: canonicalPoliciesSchema.default({}),
396
882
  /**
397
883
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -402,22 +888,55 @@ var canonicalTableSchema = z.object({
402
888
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
403
889
  cowMutations: z.array(canonicalCowMutationSchema).optional(),
404
890
  /** Omitted by old definitions, preserving their existing content hashes. */
405
- foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional()
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()
406
912
  });
913
+ var MAX_VIEW_QUERY = 1e5;
914
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
407
915
  var canonicalViewSchema = z.object({
408
- view: identifier,
916
+ view: identifier2,
409
917
  description: z.string().max(2e3).optional(),
410
918
  columns: z.array(canonicalColumnSchema).min(1),
411
919
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
412
920
  primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
413
921
  links: z.array(canonicalLinkSchema).default([]),
414
922
  policies: z.object({ read: policyExpressionSchema }).strict(),
415
- query: z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
923
+ query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
416
924
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
417
925
  }),
418
926
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
419
- 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()
420
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
+ }
421
940
  for (const [index3, column] of view.columns.entries()) {
422
941
  if (column.default !== void 0 || column.defaultFn !== void 0) {
423
942
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
@@ -432,15 +951,82 @@ var canonicalViewSchema = z.object({
432
951
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
433
952
  }
434
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
+ }
435
959
  for (const [index3, link] of view.links.entries()) {
436
960
  if (link.enforce) {
437
961
  ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
438
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
+ }
439
966
  }
440
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
+ }
441
1027
  var canonicalDefinitionSchema = z.object({
442
1028
  definition_format: z.literal(CANONICAL_FORMAT),
443
- namespace: identifier.max(41),
1029
+ namespace: identifier2.max(41),
444
1030
  description: z.string().max(2e3).optional(),
445
1031
  enums: z.array(canonicalEnumSchema).default([]),
446
1032
  tables: z.array(canonicalTableSchema).min(0).max(500),
@@ -458,6 +1044,7 @@ var canonicalDefinitionSchema = z.object({
458
1044
  ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
459
1045
  }
460
1046
  seen.add(table.table);
1047
+ validateCellMetadata(table, index3, ctx);
461
1048
  if (table.source !== void 0) {
462
1049
  for (const [columnIndex, column] of table.columns.entries()) {
463
1050
  if (column.onUpdateFn !== void 0) {
@@ -477,25 +1064,53 @@ var canonicalDefinitionSchema = z.object({
477
1064
  ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
478
1065
  ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
479
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
+ }
480
1087
  for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
481
- const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
482
- if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
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}'`);
483
1090
  names.add(fk.name);
484
1091
  const target = fkTables.get(fk.target);
485
1092
  if (table.source || !target || target.source) {
486
- fail2("foreign keys must connect owned tables in the same namespace");
1093
+ fail3("foreign keys must connect owned tables in the same namespace");
487
1094
  continue;
488
1095
  }
489
- if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
490
- if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
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");
491
1098
  for (const [position, name] of fk.columns.entries()) {
492
1099
  const sourceColumn = table.columns.find((column) => column.columnName === name);
493
1100
  const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
494
- if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
495
- else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
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");
496
1106
  }
497
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])];
498
- if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail2("foreign key target columns must form a unique key");
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 });
499
1114
  }
500
1115
  }
501
1116
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
@@ -621,8 +1236,12 @@ function canonicalRelations(definition) {
621
1236
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
622
1237
  links: view.links,
623
1238
  indexes: [],
624
- policies: { read: view.policies.read },
625
- 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 } : {}
626
1245
  }));
627
1246
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
628
1247
  }
@@ -632,13 +1251,14 @@ function normalizeCanonical(definition) {
632
1251
  ...definition,
633
1252
  enums: sortByName(definition.enums),
634
1253
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
635
- const { cowMutations, foreignKeys, ...rest } = t2;
1254
+ const { cowMutations, foreignKeys, checks, ...rest } = t2;
636
1255
  return {
637
1256
  ...rest,
638
1257
  links: sortByName(t2.links),
639
1258
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
640
1259
  actions: sortByName(t2.actions),
641
1260
  ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1261
+ ...checks?.length ? { checks: sortByName(checks) } : {},
642
1262
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
643
1263
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
644
1264
  };
@@ -925,8 +1545,19 @@ function isSeqNamespaceConfig(value) {
925
1545
  return typeof value === "object" && value !== null && value.kind === NAMESPACE_CONFIG_BRAND;
926
1546
  }
927
1547
  function serializeNamespace(input) {
928
- const { config, tables } = input;
929
- 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
+ }
930
1561
  const enums = input.enums ?? [];
931
1562
  const tablesByName = /* @__PURE__ */ new Map();
932
1563
  for (const table of tables) {
@@ -1028,6 +1659,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1028
1659
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
1029
1660
  if (table.source !== void 0) candidate.source = table.source;
1030
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
+ }
1031
1665
  if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
1032
1666
  throw new OrmDefinitionError(
1033
1667
  `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
@@ -1065,6 +1699,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1065
1699
  if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1066
1700
  candidate.columns?.push(column);
1067
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
+ }
1068
1707
  if (table.config.primaryKey !== void 0) {
1069
1708
  if (columnLevelPk.length > 0) {
1070
1709
  throw new OrmDefinitionError(
@@ -1078,6 +1717,24 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1078
1717
  } else if (columnLevelPk.length > 0) {
1079
1718
  candidate.primaryKey = columnLevelPk;
1080
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
+ }
1081
1738
  if (table.config.foreignKeys?.length) {
1082
1739
  if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
1083
1740
  candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
@@ -1090,10 +1747,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1090
1747
  columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
1091
1748
  target: target.tableName,
1092
1749
  references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
1093
- ...fk.deferrable ? { deferrable: fk.deferrable } : {}
1750
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {},
1751
+ ...fk.onDelete ? { onDelete: fk.onDelete } : {},
1752
+ ...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
1094
1753
  };
1095
1754
  });
1096
1755
  }
1756
+ if (table.config.checks?.length) {
1757
+ candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
1758
+ }
1097
1759
  for (const index3 of table.config.indexes ?? []) {
1098
1760
  const entry = {
1099
1761
  columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1189,7 +1851,33 @@ function buildCanonicalView({
1189
1851
  columns,
1190
1852
  ...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
1191
1853
  links,
1192
- 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
1193
1881
  };
1194
1882
  }
1195
1883
  function buildCanonicalLink({
@@ -1200,14 +1888,17 @@ function buildCanonicalLink({
1200
1888
  }) {
1201
1889
  const ownerKind = relationKind(relation);
1202
1890
  const ownerName = relationName(relation);
1203
- const fail2 = (message) => {
1891
+ const fail3 = (message) => {
1204
1892
  throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
1205
1893
  };
1206
1894
  if (link.enforce && link.linkKind !== "toOne") {
1207
- 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");
1208
1899
  }
1209
1900
  if (link.linkKind === "toPlatformUser") {
1210
- 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");
1211
1902
  return {
1212
1903
  name: linkName,
1213
1904
  kind: "toPlatformUser",
@@ -1217,13 +1908,13 @@ function buildCanonicalLink({
1217
1908
  ...link.description !== void 0 ? { description: link.description } : {}
1218
1909
  };
1219
1910
  }
1220
- const target = resolveRelationRef({ ref: link.target, fail: fail2 });
1911
+ const target = resolveRelationRef({ ref: link.target, fail: fail3 });
1221
1912
  const targetName = relationName(target);
1222
1913
  if (!relationsByName.has(targetName)) {
1223
- 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`);
1224
1915
  }
1225
1916
  if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
1226
- return fail2("cannot enforce a PostgreSQL foreign key involving a view");
1917
+ return fail3("cannot enforce a PostgreSQL foreign key involving a view");
1227
1918
  }
1228
1919
  const owningSide = link.linkKind === "toOne" ? relation : target;
1229
1920
  const referencedSide = link.linkKind === "toOne" ? target : relation;
@@ -1234,22 +1925,24 @@ function buildCanonicalLink({
1234
1925
  on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1235
1926
  ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
1236
1927
  enforce: link.enforce,
1928
+ ...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
1929
+ ...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
1237
1930
  ...link.description !== void 0 ? { description: link.description } : {}
1238
1931
  };
1239
1932
  }
1240
- function resolveRelationRef({ ref, fail: fail2 }) {
1241
- 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");
1242
1935
  if (typeof ref !== "function") return ref;
1243
1936
  let resolved;
1244
1937
  try {
1245
1938
  resolved = ref();
1246
1939
  } catch (error) {
1247
- return fail2(
1940
+ return fail3(
1248
1941
  `target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
1249
1942
  );
1250
1943
  }
1251
1944
  if (!isSeqTable(resolved) && !isSeqView(resolved)) {
1252
- 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");
1253
1946
  }
1254
1947
  return resolved;
1255
1948
  }
@@ -1549,6 +2242,7 @@ function tableNames(table) {
1549
2242
  updateByPkRoot: `update${rootPascal}ByPk`,
1550
2243
  deleteRoot: `delete${rootPascal}`,
1551
2244
  deleteByPkRoot: `delete${rootPascal}ByPk`,
2245
+ revertByPkRoot: `revert${rootPascal}ByPk`,
1552
2246
  boolExp: `${type}BoolExp`,
1553
2247
  orderBy: `${type}OrderBy`,
1554
2248
  selectColumn: `${type}SelectColumn`,
@@ -1574,6 +2268,17 @@ function tableNames(table) {
1574
2268
  aggregateGroup: `${type}AggregateGroup`
1575
2269
  };
1576
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
+ }
1577
2282
 
1578
2283
  // src/codegen-gql/type-policies.ts
1579
2284
  function generateTypePolicies(canonical) {
@@ -1581,13 +2286,13 @@ function generateTypePolicies(canonical) {
1581
2286
  const entries = [];
1582
2287
  let hasPlatformUser = false;
1583
2288
  for (const table of canonicalRelations(def)) {
1584
- if (table.source !== void 0) continue;
2289
+ if (table.source !== void 0 || table.internal === true) continue;
1585
2290
  if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
1586
2291
  if (!table.primaryKey || table.primaryKey.length === 0) continue;
1587
- const keyFields = table.primaryKey.map((identifier2) => {
1588
- 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);
1589
2294
  if (!column) {
1590
- 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}".`);
1591
2296
  }
1592
2297
  return fieldName(column.name);
1593
2298
  });
@@ -1604,6 +2309,88 @@ ${body}
1604
2309
  }
1605
2310
  `;
1606
2311
  }
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
+ }
1607
2394
  var IR_VERSION = 2;
1608
2395
  var LEGACY_IR_VERSION = 1;
1609
2396
  var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(IR_VERSION)]);
@@ -1687,6 +2474,21 @@ var selectionIrSchema = z.lazy(
1687
2474
  () => z.object({
1688
2475
  typename: z.string(),
1689
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(),
1690
2492
  relations: z.array(z.object({
1691
2493
  alias: z.string(),
1692
2494
  link: z.string(),
@@ -1708,6 +2510,11 @@ var selectionIrSchema = z.lazy(
1708
2510
  })
1709
2511
  );
1710
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
+ });
1711
2518
  var selectRootSchema = z.object({
1712
2519
  kind: z.literal("select"),
1713
2520
  alias: z.string(),
@@ -1719,7 +2526,8 @@ var selectRootSchema = z.object({
1719
2526
  distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
1720
2527
  limit: limitIrSchema,
1721
2528
  offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
1722
- includeIf: includeIfSchema.optional()
2529
+ includeIf: includeIfSchema.optional(),
2530
+ prefetch: z.array(prefetchPlanIrSchema).optional()
1723
2531
  });
1724
2532
  var byPkRootSchema = z.object({
1725
2533
  kind: z.literal("byPk"),
@@ -1729,7 +2537,8 @@ var byPkRootSchema = z.object({
1729
2537
  pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
1730
2538
  /** Policy predicate — ANDed with the PK match. */
1731
2539
  where: predIrSchema,
1732
- includeIf: includeIfSchema.optional()
2540
+ includeIf: includeIfSchema.optional(),
2541
+ prefetch: z.array(prefetchPlanIrSchema).optional()
1733
2542
  });
1734
2543
  var aggregateRootSchema = z.object({
1735
2544
  kind: z.literal("aggregate"),
@@ -1787,7 +2596,20 @@ var mutationRootSchema = z.object({
1787
2596
  * coalesce view falls back to synced. Both are gated on a matching synced
1788
2597
  * row and carry the write policy in `where` like update/delete.
1789
2598
  */
1790
- 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(),
1791
2613
  alias: z.string(),
1792
2614
  table: z.string(),
1793
2615
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -1872,6 +2694,43 @@ var compiledOperationSchema = z.object({
1872
2694
  }
1873
2695
  }
1874
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()
2733
+ });
1875
2734
  var tableMetaIrSchema = z.object({
1876
2735
  table: z.string(),
1877
2736
  schema: z.string(),
@@ -1912,6 +2771,22 @@ var tableMetaIrSchema = z.object({
1912
2771
  * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
1913
2772
  */
1914
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(),
1915
2790
  constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
1916
2791
  /** Depth/node caps for dynamic filter trees on this table. */
1917
2792
  dynamicFilterMaxDepth: z.number().int().positive().default(6),
@@ -1980,7 +2855,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
1980
2855
  var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
1981
2856
  var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
1982
2857
  var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
1983
- function tokenize(source) {
2858
+ function tokenize2(source) {
1984
2859
  const tokens = [];
1985
2860
  let position = 0;
1986
2861
  while (position < source.length) {
@@ -2059,7 +2934,7 @@ function readString(source, start) {
2059
2934
  }
2060
2935
  throw new PolicyParseError("unterminated string literal", start);
2061
2936
  }
2062
- var Parser = class {
2937
+ var Parser2 = class {
2063
2938
  constructor(tokens) {
2064
2939
  this.tokens = tokens;
2065
2940
  }
@@ -2233,7 +3108,7 @@ var Parser = class {
2233
3108
  function parsePolicy(source) {
2234
3109
  const trimmed = source.trim();
2235
3110
  if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
2236
- return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
3111
+ return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
2237
3112
  }
2238
3113
 
2239
3114
  // src/predicate-v2/compile.ts
@@ -2625,6 +3500,7 @@ var PolicyInjector = class {
2625
3500
  injectMutation(root) {
2626
3501
  const table = root.table;
2627
3502
  const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
3503
+ if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
2628
3504
  if (root.op === "insert" || root.op === "insertOne") {
2629
3505
  const insertPolicy = this.insert(table);
2630
3506
  if (predContainsExists(insertPolicy)) {
@@ -2666,6 +3542,24 @@ var PolicyInjector = class {
2666
3542
  }
2667
3543
  return { ...root, where, returning };
2668
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
+ }
2669
3563
  };
2670
3564
  function collectTableSet(roots, ctx) {
2671
3565
  const tables = /* @__PURE__ */ new Set();
@@ -2725,6 +3619,7 @@ function collectTableSet(roots, ctx) {
2725
3619
  break;
2726
3620
  case "mutation":
2727
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);
2728
3623
  if (root.where) visitPred(root.where, root.table);
2729
3624
  if (root.insertCheck) visitPred(root.insertCheck, root.table);
2730
3625
  if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
@@ -2846,7 +3741,8 @@ function resolveColumnEntries(table, enums, diagnostics) {
2846
3741
  type,
2847
3742
  nullable: column.nullable,
2848
3743
  deprecated: column.deprecated,
2849
- onUpdateFn: column.onUpdateFn !== void 0
3744
+ onUpdateFn: column.onUpdateFn !== void 0,
3745
+ ...column.cell ? { cell: column.cell } : {}
2850
3746
  };
2851
3747
  });
2852
3748
  }
@@ -2883,8 +3779,8 @@ function buildCompileContext(rawCanonical) {
2883
3779
  const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
2884
3780
  const primaryKey2 = [];
2885
3781
  for (const ref of pkRefs) {
2886
- const resolved = logicalColumn(entries, ref);
2887
- if (!resolved) {
3782
+ const resolved2 = logicalColumn(entries, ref);
3783
+ if (!resolved2) {
2888
3784
  diagnostics.push({
2889
3785
  severity: "error",
2890
3786
  code: "unknown-pk-column",
@@ -2893,9 +3789,12 @@ function buildCompileContext(rawCanonical) {
2893
3789
  });
2894
3790
  continue;
2895
3791
  }
2896
- primaryKey2.push(resolved);
3792
+ primaryKey2.push(resolved2);
2897
3793
  }
2898
- 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);
2899
3798
  const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
2900
3799
  const constraints = [];
2901
3800
  if (primaryKey2.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey2] });
@@ -2920,12 +3819,14 @@ function buildCompileContext(rawCanonical) {
2920
3819
  columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
2921
3820
  primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
2922
3821
  links: [],
2923
- filterable: entries.map((c) => c.name),
2924
- orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
2925
- insertable: isView ? [] : active.map((c) => c.name),
2926
- settable: isView ? [] : active.filter((c) => !c.onUpdateFn).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),
2927
3826
  appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
2928
3827
  ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
3828
+ ...cells ? { cells } : {},
3829
+ ...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
2929
3830
  constraints: isView ? [] : constraints,
2930
3831
  dynamicFilterMaxDepth: 6,
2931
3832
  dynamicFilterMaxNodes: 64
@@ -2998,7 +3899,7 @@ function buildCompileContext(rawCanonical) {
2998
3899
  compilePolicyFor("write", writePolicies);
2999
3900
  compilePolicyFor("insert", insertPolicies);
3000
3901
  compilePolicyFor("update", updatePolicies);
3001
- if (isView) {
3902
+ if (isView && table.resolved === void 0) {
3002
3903
  writePolicies.set(table.table, CONST_FALSE);
3003
3904
  insertPolicies.set(table.table, CONST_FALSE);
3004
3905
  updatePolicies.set(table.table, CONST_FALSE);
@@ -3037,6 +3938,7 @@ function buildCompileContext(rawCanonical) {
3037
3938
  const queryRoots = /* @__PURE__ */ new Map();
3038
3939
  const mutationRoots = /* @__PURE__ */ new Map();
3039
3940
  for (const table of relations) {
3941
+ if (table.internal === true) continue;
3040
3942
  const roots = queryRootNames(table.table);
3041
3943
  queryRoots.set(roots.select, { kind: "select", table: table.table });
3042
3944
  if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
@@ -3052,6 +3954,7 @@ function buildCompileContext(rawCanonical) {
3052
3954
  mutationRoots.set(field, { op, table: table.table });
3053
3955
  }
3054
3956
  }
3957
+ if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
3055
3958
  }
3056
3959
  for (const table of relations) {
3057
3960
  for (const mutation of table.cowMutations ?? []) {
@@ -3122,6 +4025,49 @@ function buildCompileContext(rawCanonical) {
3122
4025
  diagnostics
3123
4026
  };
3124
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
+ }
3125
4071
  function flattenSelectionSet(selectionSet, fragments, stack) {
3126
4072
  const selections = [];
3127
4073
  for (const selection of selectionSet.selections) {
@@ -3179,6 +4125,136 @@ function computeOpsHash(opHashes) {
3179
4125
  const digest = createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
3180
4126
  return `sha256:${digest}`;
3181
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
3182
4258
  var DEFAULT_LIMIT = 100;
3183
4259
  var MAX_LIMIT = 1e3;
3184
4260
  var MAX_GROUPS = 1e3;
@@ -3216,7 +4292,7 @@ var ORDER_DIRECTIONS = {
3216
4292
  DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
3217
4293
  };
3218
4294
  var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
3219
- function fail(message, code, candidates = []) {
4295
+ function fail2(message, code, candidates = []) {
3220
4296
  throw new DocumentCompileError(message, code, candidates);
3221
4297
  }
3222
4298
  function argsByName(field) {
@@ -3224,16 +4300,16 @@ function argsByName(field) {
3224
4300
  }
3225
4301
  function* aggregateBlockFields(selectionSet) {
3226
4302
  for (const node of selectionSet.selections) {
3227
- if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
4303
+ if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3228
4304
  if (node.name.value === "__typename") continue;
3229
- if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
4305
+ if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
3230
4306
  yield node;
3231
4307
  }
3232
4308
  }
3233
4309
  function assertKnownArgs(field, allowed) {
3234
4310
  for (const arg of field.arguments ?? []) {
3235
4311
  if (!allowed.includes(arg.name.value)) {
3236
- 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);
3237
4313
  }
3238
4314
  }
3239
4315
  }
@@ -3246,8 +4322,11 @@ var OperationLowerer = class {
3246
4322
  meta(table) {
3247
4323
  return this.ctx.tables[table];
3248
4324
  }
4325
+ /** A data column — cell-metadata columns are reachable only through `cells { … }`. */
3249
4326
  column(table, name) {
3250
- 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;
3251
4330
  }
3252
4331
  link(table, name) {
3253
4332
  return this.meta(table).links.find((l) => l.name === name);
@@ -3255,7 +4334,8 @@ var OperationLowerer = class {
3255
4334
  fieldCandidates(table) {
3256
4335
  const meta = this.meta(table);
3257
4336
  return [
3258
- ...meta.columns.map((c) => c.name),
4337
+ ...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
4338
+ ...meta.cells ? [CELLS_FIELD] : [],
3259
4339
  ...meta.links.map((l) => l.name),
3260
4340
  ...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
3261
4341
  ];
@@ -3264,7 +4344,7 @@ var OperationLowerer = class {
3264
4344
  slot(name, input, table) {
3265
4345
  const variable = this.variables.get(name);
3266
4346
  if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
3267
- 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");
3268
4348
  }
3269
4349
  return name;
3270
4350
  }
@@ -3289,14 +4369,14 @@ var OperationLowerer = class {
3289
4369
  const items = node.values.map((item) => {
3290
4370
  const lowered = this.lowerValue(item);
3291
4371
  if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
3292
- 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");
3293
4373
  }
3294
4374
  return lowered.value;
3295
4375
  });
3296
4376
  return { kind: "literal", value: items };
3297
4377
  }
3298
4378
  case Kind.OBJECT:
3299
- 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");
3300
4380
  }
3301
4381
  }
3302
4382
  // -------------------------------------------------------------------------
@@ -3306,7 +4386,7 @@ var OperationLowerer = class {
3306
4386
  if (node.kind === Kind.VARIABLE) {
3307
4387
  return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
3308
4388
  }
3309
- 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");
3310
4390
  const operands = [];
3311
4391
  for (const field of node.fields) {
3312
4392
  const key = field.name.value;
@@ -3337,45 +4417,45 @@ var OperationLowerer = class {
3337
4417
  continue;
3338
4418
  }
3339
4419
  }
3340
- 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));
3341
4421
  }
3342
4422
  if (operands.length === 0) return CONST_TRUE;
3343
4423
  return operands.length === 1 ? operands[0] : { kind: "and", operands };
3344
4424
  }
3345
4425
  lowerComparison(node, column, table) {
3346
4426
  if (node.kind !== Kind.OBJECT) {
3347
- 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));
3348
4428
  }
3349
4429
  return node.fields.map((field) => {
3350
4430
  const op = BOOLEXP_OPS[field.name.value];
3351
4431
  if (!op) {
3352
- 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));
3353
4433
  }
3354
4434
  return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
3355
4435
  });
3356
4436
  }
3357
4437
  /** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
3358
4438
  lowerAggPredicate(node, link, target) {
3359
- 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");
3360
4440
  const countField = node.fields.find((f) => f.name.value === "count");
3361
4441
  if (!countField || node.fields.length !== 1) {
3362
- 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"]);
3363
4443
  }
3364
- 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");
3365
4445
  let predicate;
3366
4446
  let where;
3367
4447
  for (const field of countField.value.fields) {
3368
4448
  if (field.name.value === "predicate") {
3369
4449
  if (field.value.kind !== Kind.OBJECT || field.value.fields.length !== 1) {
3370
- 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]);
3371
4451
  }
3372
4452
  const opField = field.value.fields[0];
3373
4453
  const opName = opField.name.value;
3374
4454
  if (!AGG_PREDICATE_OPS.includes(opName)) {
3375
- 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");
3376
4456
  }
3377
4457
  if (opField.value.kind !== Kind.INT) {
3378
- 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");
3379
4459
  }
3380
4460
  predicate = { op: opName.slice(1), value: Number(opField.value.value) };
3381
4461
  continue;
@@ -3384,9 +4464,9 @@ var OperationLowerer = class {
3384
4464
  where = this.lowerBoolExp(field.value, target);
3385
4465
  continue;
3386
4466
  }
3387
- 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"]);
3388
4468
  }
3389
- 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");
3390
4470
  return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
3391
4471
  }
3392
4472
  lowerOrderBy(node, table) {
@@ -3396,7 +4476,7 @@ var OperationLowerer = class {
3396
4476
  const items = node.kind === Kind.LIST ? node.values : [node];
3397
4477
  const lowered = [];
3398
4478
  for (const item of items) {
3399
- 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");
3400
4480
  for (const field of item.fields) {
3401
4481
  const key = field.name.value;
3402
4482
  const column = this.column(table, key);
@@ -3407,25 +4487,25 @@ var OperationLowerer = class {
3407
4487
  const link = this.link(table, key);
3408
4488
  if (link && link.kind !== "toMany") {
3409
4489
  if (field.value.kind !== Kind.OBJECT || field.value.fields.length !== 1) {
3410
- fail(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
4490
+ fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
3411
4491
  }
3412
4492
  const inner = field.value.fields[0];
3413
4493
  const targetColumn = this.column(link.target, inner.name.value);
3414
4494
  if (!targetColumn) {
3415
- 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));
3416
4496
  }
3417
4497
  lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
3418
4498
  continue;
3419
4499
  }
3420
- 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));
3421
4501
  }
3422
4502
  }
3423
4503
  return lowered;
3424
4504
  }
3425
4505
  lowerDirection(node, table, column) {
3426
- 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));
3427
4507
  const direction = ORDER_DIRECTIONS[node.value];
3428
- 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));
3429
4509
  return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
3430
4510
  }
3431
4511
  lowerDistinctOn(node, table) {
@@ -3434,9 +4514,9 @@ var OperationLowerer = class {
3434
4514
  }
3435
4515
  const items = node.kind === Kind.LIST ? node.values : [node];
3436
4516
  return items.map((item) => {
3437
- 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");
3438
4518
  if (!this.column(table, item.value)) {
3439
- 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));
3440
4520
  }
3441
4521
  return item.value;
3442
4522
  });
@@ -3447,16 +4527,16 @@ var OperationLowerer = class {
3447
4527
  if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
3448
4528
  if (node.kind === Kind.INT) {
3449
4529
  const value = Number(node.value);
3450
- 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");
3451
4531
  return { ...base, value: Math.min(value, MAX_LIMIT) };
3452
4532
  }
3453
- 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");
3454
4534
  }
3455
4535
  lowerOffset(node) {
3456
4536
  if (!node) return void 0;
3457
4537
  if (node.kind === Kind.VARIABLE) return { variable: node.name.value };
3458
4538
  if (node.kind === Kind.INT) return { value: Number(node.value) };
3459
- 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");
3460
4540
  }
3461
4541
  // -------------------------------------------------------------------------
3462
4542
  // Selections
@@ -3465,9 +4545,9 @@ var OperationLowerer = class {
3465
4545
  const meta = this.meta(table);
3466
4546
  const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
3467
4547
  for (const node of selectionSet.selections) {
3468
- 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");
3469
4549
  if (node.directives && node.directives.length > 0) {
3470
- 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");
3471
4551
  }
3472
4552
  const name = node.name.value;
3473
4553
  if (name === "__typename") continue;
@@ -3475,14 +4555,19 @@ var OperationLowerer = class {
3475
4555
  const column = this.column(table, name);
3476
4556
  if (column) {
3477
4557
  if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
3478
- 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");
3479
4559
  }
3480
4560
  selection.columns.push({ alias, column: name });
3481
4561
  continue;
3482
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
+ }
3483
4568
  const link = this.link(table, name);
3484
4569
  if (link) {
3485
- 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");
3486
4571
  const relation = {
3487
4572
  alias,
3488
4573
  link: name,
@@ -3501,7 +4586,7 @@ var OperationLowerer = class {
3501
4586
  if (offset) relation.offset = offset;
3502
4587
  }
3503
4588
  } else if (node.arguments && node.arguments.length > 0) {
3504
- fail(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
4589
+ fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
3505
4590
  }
3506
4591
  selection.relations.push(relation);
3507
4592
  continue;
@@ -3509,7 +4594,7 @@ var OperationLowerer = class {
3509
4594
  if (name.endsWith("Aggregate")) {
3510
4595
  const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
3511
4596
  if (aggLink && aggLink.kind === "toMany") {
3512
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4597
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3513
4598
  assertKnownArgs(node, ["where"]);
3514
4599
  const whereArg = argsByName(node).get("where");
3515
4600
  const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
@@ -3522,10 +4607,57 @@ var OperationLowerer = class {
3522
4607
  continue;
3523
4608
  }
3524
4609
  }
3525
- fail(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
4610
+ fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
3526
4611
  }
3527
4612
  return selection;
3528
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
+ }
3529
4661
  /** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
3530
4662
  lowerAggregateSelection(selectionSet, table) {
3531
4663
  const result = {};
@@ -3533,17 +4665,17 @@ var OperationLowerer = class {
3533
4665
  for (const node of aggregateBlockFields(selectionSet)) {
3534
4666
  const name = node.name.value;
3535
4667
  if (name === "nodes") {
3536
- if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
4668
+ if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
3537
4669
  result.nodes = this.lowerSelection(node.selectionSet, table);
3538
4670
  continue;
3539
4671
  }
3540
- if (name !== "aggregate") fail(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
3541
- if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
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");
3542
4674
  sawAggregate = true;
3543
- if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
4675
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
3544
4676
  Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
3545
4677
  }
3546
- if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
4678
+ if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
3547
4679
  return result;
3548
4680
  }
3549
4681
  /**
@@ -3560,24 +4692,24 @@ var OperationLowerer = class {
3560
4692
  for (const node of aggregateBlockFields(selectionSet)) {
3561
4693
  const name = node.name.value;
3562
4694
  if (name === "key") {
3563
- if (sawKey) fail("one key block per grouped aggregate", "invalid-aggregate");
4695
+ if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
3564
4696
  sawKey = true;
3565
- if (!node.selectionSet) fail("'key' needs a selection set", "invalid-aggregate");
4697
+ if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
3566
4698
  result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
3567
4699
  continue;
3568
4700
  }
3569
4701
  if (name === "nodes") {
3570
- fail("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
4702
+ fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
3571
4703
  }
3572
- if (name !== "aggregate") fail(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
3573
- if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
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");
3574
4706
  sawAggregate = true;
3575
- if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
4707
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
3576
4708
  Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
3577
4709
  }
3578
4710
  const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
3579
4711
  if (result.key.length === 0 && !rendersAggregate) {
3580
- fail(
4712
+ fail2(
3581
4713
  "grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
3582
4714
  "invalid-aggregate",
3583
4715
  ["key", "aggregate"]
@@ -3594,14 +4726,14 @@ var OperationLowerer = class {
3594
4726
  lowerGroupKeySelection(selectionSet, table, groupColumns) {
3595
4727
  const key = [];
3596
4728
  for (const node of selectionSet.selections) {
3597
- 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");
3598
4730
  const name = node.name.value;
3599
4731
  if (name === "__typename") continue;
3600
4732
  if (!this.column(table, name)) {
3601
- fail(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
4733
+ fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
3602
4734
  }
3603
4735
  if (!groupColumns.has(name)) {
3604
- fail(
4736
+ fail2(
3605
4737
  `key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
3606
4738
  "invalid-group-key",
3607
4739
  [...groupColumns]
@@ -3624,21 +4756,21 @@ var OperationLowerer = class {
3624
4756
  if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
3625
4757
  const distinct = args.get("distinct");
3626
4758
  if (distinct) {
3627
- if (distinct.value.kind !== Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
4759
+ if (distinct.value.kind !== Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
3628
4760
  count.distinct = distinct.value.value;
3629
4761
  }
3630
4762
  result.count = count;
3631
4763
  continue;
3632
4764
  }
3633
4765
  if (aggName === "sum" || aggName === "avg") {
3634
- if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
4766
+ if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
3635
4767
  const columns = [];
3636
4768
  for (const columnField of aggField.selectionSet.selections) {
3637
- if (columnField.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
4769
+ if (columnField.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3638
4770
  if (columnField.name.value === "__typename") continue;
3639
4771
  const column = this.column(table, columnField.name.value);
3640
4772
  if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
3641
- fail(
4773
+ fail2(
3642
4774
  `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
3643
4775
  "invalid-aggregate",
3644
4776
  this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
@@ -3649,7 +4781,7 @@ var OperationLowerer = class {
3649
4781
  result[aggName] = columns;
3650
4782
  continue;
3651
4783
  }
3652
- fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
4784
+ fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
3653
4785
  }
3654
4786
  return result;
3655
4787
  }
@@ -3659,41 +4791,43 @@ var OperationLowerer = class {
3659
4791
  lowerRoots(operation, kind) {
3660
4792
  const roots = [];
3661
4793
  for (const node of operation.selectionSet.selections) {
3662
- 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");
3663
4795
  if (node.selectionSet) visit(node.selectionSet, {
3664
4796
  Directive(directive) {
3665
4797
  if (directive.name.value === "assertAffectedRows") {
3666
- fail("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
4798
+ fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
3667
4799
  }
3668
4800
  }
3669
4801
  });
3670
4802
  const assertion = this.parseAffectedRowAssertion(node, kind);
3671
4803
  const gate = this.parseRootDirectives(node);
3672
4804
  if (gate.omit) continue;
3673
- const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
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 });
3674
4808
  const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
3675
4809
  roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
3676
4810
  }
3677
- 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");
3678
4812
  return roots;
3679
4813
  }
3680
4814
  parseAffectedRowAssertion(node, kind) {
3681
4815
  const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
3682
4816
  if (!assertions.length) return void 0;
3683
4817
  if (kind !== "mutation" || assertions.length !== 1) {
3684
- fail("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
4818
+ fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
3685
4819
  }
3686
4820
  if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
3687
- fail("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
4821
+ fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
3688
4822
  }
3689
4823
  const args = assertions[0].arguments;
3690
4824
  const arg = args?.[0];
3691
4825
  if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== Kind.INT) {
3692
- fail("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
4826
+ fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
3693
4827
  }
3694
4828
  const count = Number(arg.value.value);
3695
4829
  if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
3696
- fail("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
4830
+ fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
3697
4831
  }
3698
4832
  return count;
3699
4833
  }
@@ -3703,18 +4837,19 @@ var OperationLowerer = class {
3703
4837
  for (const directive of node.directives ?? []) {
3704
4838
  const name = directive.name.value;
3705
4839
  if (name === "assertAffectedRows") continue;
4840
+ if (SOURCE_DIRECTIVES.has(name)) continue;
3706
4841
  if (name !== "include" && name !== "skip") {
3707
- 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"]);
3708
4843
  }
3709
4844
  const ifArg = directive.arguments?.find((a) => a.name.value === "if");
3710
- if (!ifArg) fail(`@${name} needs an if: argument`, "invalid-directive");
4845
+ if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
3711
4846
  if (ifArg.value.kind === Kind.BOOLEAN) {
3712
4847
  const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
3713
4848
  if (!keep) omit = true;
3714
4849
  continue;
3715
4850
  }
3716
- if (ifArg.value.kind !== Kind.VARIABLE) fail(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
3717
- 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");
3718
4853
  includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
3719
4854
  }
3720
4855
  return omit ? { omit: true } : { includeIf, omit: false };
@@ -3723,12 +4858,12 @@ var OperationLowerer = class {
3723
4858
  const name = node.name.value;
3724
4859
  const alias = node.alias?.value ?? name;
3725
4860
  const rootInfo = this.ctx.queryRoots.get(name);
3726
- 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()]);
3727
4862
  const { table } = rootInfo;
3728
4863
  const args = argsByName(node);
3729
4864
  if (rootInfo.kind === "select") {
3730
4865
  assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
3731
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4866
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3732
4867
  const where = args.get("where");
3733
4868
  const orderBy = args.get("orderBy");
3734
4869
  const distinctOn = args.get("distinctOn");
@@ -3747,7 +4882,7 @@ var OperationLowerer = class {
3747
4882
  };
3748
4883
  }
3749
4884
  if (rootInfo.kind === "byPk") {
3750
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4885
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3751
4886
  const pk = this.lowerPkArgs(node, table);
3752
4887
  return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
3753
4888
  }
@@ -3755,7 +4890,7 @@ var OperationLowerer = class {
3755
4890
  return this.lowerAggregateGroupsRoot(node, { alias, table });
3756
4891
  }
3757
4892
  assertKnownArgs(node, ["where"]);
3758
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4893
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3759
4894
  const whereArg = args.get("where");
3760
4895
  const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
3761
4896
  return {
@@ -3769,10 +4904,10 @@ var OperationLowerer = class {
3769
4904
  /** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
3770
4905
  lowerAggregateGroupsRoot(node, { alias, table }) {
3771
4906
  assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
3772
- if (!node.selectionSet) fail(`'${node.name.value}' needs a selection set`, "invalid-selection");
4907
+ if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
3773
4908
  const args = argsByName(node);
3774
4909
  const groupByArg = args.get("groupBy");
3775
- if (!groupByArg) fail(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
4910
+ if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
3776
4911
  const whereArg = args.get("where");
3777
4912
  const groupBy = this.lowerGroupBy(groupByArg.value, table);
3778
4913
  const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
@@ -3802,55 +4937,55 @@ var OperationLowerer = class {
3802
4937
  */
3803
4938
  lowerGroupBy(node, table) {
3804
4939
  if (node.kind === Kind.VARIABLE) {
3805
- fail(
4940
+ fail2(
3806
4941
  `groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
3807
4942
  "dynamic-group-by"
3808
4943
  );
3809
4944
  }
3810
4945
  const items = node.kind === Kind.LIST ? node.values : [node];
3811
- if (items.length === 0) fail(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
4946
+ if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
3812
4947
  const seen = /* @__PURE__ */ new Set();
3813
4948
  return items.map((item) => {
3814
4949
  if (item.kind !== Kind.OBJECT) {
3815
- fail(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
4950
+ fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
3816
4951
  }
3817
4952
  const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
3818
4953
  for (const key of fields.keys()) {
3819
4954
  if (key !== "column" && key !== "bucket") {
3820
- fail(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
4955
+ fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
3821
4956
  }
3822
4957
  }
3823
4958
  const columnNode = fields.get("column");
3824
- if (!columnNode) fail(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
4959
+ if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
3825
4960
  if (columnNode.kind !== Kind.ENUM) {
3826
- fail(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
4961
+ fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
3827
4962
  }
3828
4963
  const column = this.column(table, columnNode.value);
3829
4964
  if (!column) {
3830
- fail(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
4965
+ fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3831
4966
  }
3832
4967
  if (column.type === "jsonb") {
3833
- fail(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
4968
+ fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
3834
4969
  }
3835
4970
  const bucketNode = fields.get("bucket");
3836
4971
  let bucket;
3837
4972
  if (bucketNode) {
3838
4973
  if (bucketNode.kind !== Kind.ENUM) {
3839
- fail(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
4974
+ fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3840
4975
  }
3841
4976
  bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
3842
4977
  if (!bucket) {
3843
- fail(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
4978
+ fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3844
4979
  }
3845
4980
  if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
3846
- fail(
4981
+ fail2(
3847
4982
  `groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
3848
4983
  "invalid-group-by"
3849
4984
  );
3850
4985
  }
3851
4986
  }
3852
4987
  if (seen.has(columnNode.value)) {
3853
- fail(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
4988
+ fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
3854
4989
  }
3855
4990
  seen.add(columnNode.value);
3856
4991
  return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
@@ -3863,10 +4998,10 @@ var OperationLowerer = class {
3863
4998
  if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
3864
4999
  if (node.kind === Kind.INT) {
3865
5000
  const value = Number(node.value);
3866
- if (value < 1) fail("limit must be a positive integer", "invalid-limit");
5001
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
3867
5002
  return { ...base, value: Math.min(value, MAX_GROUPS) };
3868
5003
  }
3869
- fail("limit must be an integer literal or a variable", "invalid-limit");
5004
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
3870
5005
  }
3871
5006
  /** byPk-style pk arguments: one argument per primary-key column, exactly. */
3872
5007
  lowerPkArgs(node, table) {
@@ -3875,7 +5010,7 @@ var OperationLowerer = class {
3875
5010
  assertKnownArgs(node, meta.primaryKey);
3876
5011
  return meta.primaryKey.map((column) => {
3877
5012
  const arg = args.get(column);
3878
- 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);
3879
5014
  return { column, value: this.lowerValue(arg.value) };
3880
5015
  });
3881
5016
  }
@@ -3886,19 +5021,22 @@ var OperationLowerer = class {
3886
5021
  const name = node.name.value;
3887
5022
  const alias = node.alias?.value ?? name;
3888
5023
  const rootInfo = this.ctx.mutationRoots.get(name);
3889
- 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()]);
3890
5025
  if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
3891
5026
  return this.lowerCowRoot(node, rootInfo);
3892
5027
  }
5028
+ if (rootInfo.op === "resolvedWrite") {
5029
+ return this.lowerResolvedRoot(node, rootInfo);
5030
+ }
3893
5031
  const { op, table } = rootInfo;
3894
5032
  const args = argsByName(node);
3895
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
5033
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3896
5034
  const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
3897
5035
  switch (op) {
3898
5036
  case "insert": {
3899
5037
  assertKnownArgs(node, ["objects", "onConflict"]);
3900
5038
  const objects = args.get("objects");
3901
- if (!objects) fail(`'${name}' needs objects:`, "missing-argument");
5039
+ if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
3902
5040
  const response = this.lowerMutationResponse(node.selectionSet, table);
3903
5041
  return {
3904
5042
  ...base,
@@ -3910,7 +5048,7 @@ var OperationLowerer = class {
3910
5048
  case "insertOne": {
3911
5049
  assertKnownArgs(node, ["object", "onConflict"]);
3912
5050
  const object = args.get("object");
3913
- if (!object) fail(`'${name}' needs object:`, "missing-argument");
5051
+ if (!object) fail2(`'${name}' needs object:`, "missing-argument");
3914
5052
  return {
3915
5053
  ...base,
3916
5054
  objects: this.lowerInsertObjects(object.value, table, { list: false }),
@@ -3921,7 +5059,7 @@ var OperationLowerer = class {
3921
5059
  case "update": {
3922
5060
  assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
3923
5061
  const where = args.get("where");
3924
- 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");
3925
5063
  return {
3926
5064
  ...base,
3927
5065
  ...this.lowerMutationResponse(node.selectionSet, table),
@@ -3932,7 +5070,7 @@ var OperationLowerer = class {
3932
5070
  case "updateByPk": {
3933
5071
  assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
3934
5072
  const pkColumns = args.get("pkColumns");
3935
- if (!pkColumns) fail(`'${name}' needs pkColumns:`, "missing-argument");
5073
+ if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
3936
5074
  return {
3937
5075
  ...base,
3938
5076
  pk: this.lowerPkColumnsArg(pkColumns.value, table),
@@ -3943,7 +5081,7 @@ var OperationLowerer = class {
3943
5081
  case "delete": {
3944
5082
  assertKnownArgs(node, ["where"]);
3945
5083
  const where = args.get("where");
3946
- 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");
3947
5085
  return {
3948
5086
  ...base,
3949
5087
  ...this.lowerMutationResponse(node.selectionSet, table),
@@ -3970,11 +5108,11 @@ var OperationLowerer = class {
3970
5108
  const name = node.name.value;
3971
5109
  const alias = node.alias?.value ?? name;
3972
5110
  const { table } = rootInfo;
3973
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
5111
+ if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
3974
5112
  const meta = this.meta(table);
3975
5113
  const args = argsByName(node);
3976
5114
  const pkArg = args.get(rootInfo.pk);
3977
- 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]);
3978
5116
  const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
3979
5117
  const base = {
3980
5118
  kind: "mutation",
@@ -3995,24 +5133,78 @@ var OperationLowerer = class {
3995
5133
  const entries = rootInfo.set.map((column) => {
3996
5134
  const arg = args.get(column);
3997
5135
  if (!arg) {
3998
- fail(
5136
+ fail2(
3999
5137
  `'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
4000
5138
  "missing-argument",
4001
5139
  [...rootInfo.set]
4002
5140
  );
4003
5141
  }
4004
5142
  if (!meta.settable.includes(column)) {
4005
- 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);
4006
5144
  }
4007
5145
  return { column, value: this.lowerValue(arg.value) };
4008
5146
  });
4009
5147
  return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
4010
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
+ }
4011
5203
  lowerMutationResponse(selectionSet, table) {
4012
5204
  let affectedRows = false;
4013
5205
  let returning = null;
4014
5206
  for (const node of selectionSet.selections) {
4015
- 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");
4016
5208
  const name = node.name.value;
4017
5209
  if (name === "__typename") continue;
4018
5210
  if (name === "affectedRows") {
@@ -4020,34 +5212,34 @@ var OperationLowerer = class {
4020
5212
  continue;
4021
5213
  }
4022
5214
  if (name === "returning") {
4023
- if (!node.selectionSet) fail("'returning' needs a selection set", "invalid-selection");
5215
+ if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
4024
5216
  returning = this.lowerSelection(node.selectionSet, table);
4025
5217
  continue;
4026
5218
  }
4027
- fail(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
5219
+ fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
4028
5220
  }
4029
5221
  return { affectedRows, returning };
4030
5222
  }
4031
5223
  lowerInsertObjects(node, table, { list }) {
4032
5224
  if (node.kind === Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
4033
5225
  if (list) {
4034
- 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");
4035
5227
  return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
4036
5228
  }
4037
5229
  return { rows: [this.lowerInsertRow(node, table)] };
4038
5230
  }
4039
5231
  lowerInsertRow(node, table) {
4040
- 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");
4041
5233
  const meta = this.meta(table);
4042
5234
  return node.fields.map((field) => {
4043
5235
  const key = field.name.value;
4044
5236
  if (this.link(table, key)) {
4045
- 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");
4046
5238
  }
4047
5239
  if (!meta.insertable.includes(key)) {
4048
5240
  const column = this.column(table, key);
4049
- if (column) fail(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
4050
- 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);
4051
5243
  }
4052
5244
  return { column: key, value: this.lowerValue(field.value) };
4053
5245
  });
@@ -4056,32 +5248,32 @@ var OperationLowerer = class {
4056
5248
  if (!arg) return {};
4057
5249
  const node = arg.value;
4058
5250
  if (node.kind === Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
4059
- 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");
4060
5252
  const meta = this.meta(table);
4061
5253
  let constraint;
4062
5254
  const updateColumns = [];
4063
5255
  for (const field of node.fields) {
4064
5256
  if (field.name.value === "constraint") {
4065
- 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");
4066
5258
  constraint = field.value.value;
4067
5259
  continue;
4068
5260
  }
4069
5261
  if (field.name.value === "updateColumns") {
4070
5262
  const items = field.value.kind === Kind.LIST ? field.value.values : [field.value];
4071
5263
  for (const item of items) {
4072
- 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");
4073
5265
  if (!meta.settable.includes(item.value)) {
4074
- 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);
4075
5267
  }
4076
5268
  updateColumns.push(item.value);
4077
5269
  }
4078
5270
  continue;
4079
5271
  }
4080
- 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"]);
4081
5273
  }
4082
- 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));
4083
5275
  if (!meta.constraints.some((c) => c.name === constraint)) {
4084
- 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));
4085
5277
  }
4086
5278
  return { onConflict: { constraint, updateColumns } };
4087
5279
  }
@@ -4099,36 +5291,36 @@ var OperationLowerer = class {
4099
5291
  const inc = args.get("_inc");
4100
5292
  if (inc) {
4101
5293
  if (inc.value.kind === Kind.VARIABLE) {
4102
- 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");
4103
5295
  }
4104
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);
4105
5297
  result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric4, "_inc") };
4106
5298
  }
4107
5299
  if (!result.set && !result.append && !result.inc) {
4108
- 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");
4109
5301
  }
4110
5302
  return result;
4111
5303
  }
4112
5304
  lowerColumnEntries(node, table, whitelist, argName) {
4113
- 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");
4114
5306
  return node.fields.map((field) => {
4115
5307
  const key = field.name.value;
4116
5308
  if (!whitelist.includes(key)) {
4117
- 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]);
4118
5310
  }
4119
5311
  return { column: key, value: this.lowerValue(field.value) };
4120
5312
  });
4121
5313
  }
4122
5314
  lowerPkColumnsArg(node, table) {
4123
- 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");
4124
5316
  const meta = this.meta(table);
4125
5317
  const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
4126
5318
  for (const key of provided.keys()) {
4127
- 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);
4128
5320
  }
4129
5321
  return meta.primaryKey.map((column) => {
4130
5322
  const value = provided.get(column);
4131
- 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);
4132
5324
  return { column, value: this.lowerValue(value) };
4133
5325
  });
4134
5326
  }
@@ -4137,6 +5329,19 @@ function lowerOperation({ restricted, ctx }) {
4137
5329
  const lowerer = new OperationLowerer(ctx, restricted);
4138
5330
  return lowerer.lowerRoots(restricted.definition, restricted.kind);
4139
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
+ }
4140
5345
  function parseSdl(sdl) {
4141
5346
  return buildSchema(sdl);
4142
5347
  }
@@ -4513,15 +5718,6 @@ function columnWireType(column) {
4513
5718
  const notNull = column.primary_key === true || column.nullable === false;
4514
5719
  return notNull || base === "unknown" ? base : `${base} | null`;
4515
5720
  }
4516
-
4517
- // src/compiler/sql.ts
4518
- var CLAIMS_SETTING = "request.jwt.claims";
4519
- var CAPABILITIES_CLAIM_KEY = "capabilities";
4520
- var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
4521
- var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
4522
- var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
4523
- var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
4524
- var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
4525
5721
  var identifierSchema = z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
4526
5722
  var namespaceSchema = z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
4527
5723
  var tableReferenceSchema = z.string().refine(
@@ -4638,7 +5834,7 @@ var OPERATOR_PATTERN2 = /^(!=|<=|>=|<>|=|<|>)/;
4638
5834
  var IDENTIFIER_START2 = /[a-z_]/;
4639
5835
  var IDENTIFIER_CHAR2 = /[a-z0-9_.-]/;
4640
5836
  var NUMBER_PATTERN2 = /^-?\d+(\.\d+)?/;
4641
- function tokenize2(source) {
5837
+ function tokenize3(source) {
4642
5838
  const tokens = [];
4643
5839
  let position = 0;
4644
5840
  while (position < source.length) {
@@ -4712,7 +5908,7 @@ function readString2(source, start) {
4712
5908
  }
4713
5909
  throw new PredicateParseError("unterminated string literal", start);
4714
5910
  }
4715
- var Parser2 = class {
5911
+ var Parser3 = class {
4716
5912
  constructor(tokens) {
4717
5913
  this.tokens = tokens;
4718
5914
  }
@@ -4844,7 +6040,7 @@ var Parser2 = class {
4844
6040
  function parsePredicate(source) {
4845
6041
  const trimmed = source.trim();
4846
6042
  if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
4847
- return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
6043
+ return { source: trimmed, ast: new Parser3(tokenize3(trimmed)).parse() };
4848
6044
  }
4849
6045
 
4850
6046
  // src/schema/predicates.ts
@@ -5566,6 +6762,88 @@ function snapshotTableNames(snapshot, schema) {
5566
6762
  return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
5567
6763
  }
5568
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
+
5569
6847
  // src/engine/canonical2.ts
5570
6848
  var FACTORIES2 = {
5571
6849
  uuid: (name) => uuid(name),
@@ -5857,8 +7135,10 @@ function generateSnapshotV2(definition, previousId3) {
5857
7135
  const snapshot = generateDrizzleJson(imports, previousId3, [schemaName]);
5858
7136
  if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
5859
7137
  const extended = snapshot;
7138
+ const refreshFunctions = materializedRefreshFunctions(definition);
5860
7139
  extended._sequenceOrm = {
5861
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) } : {},
5862
7142
  viewDependencies: Object.fromEntries(
5863
7143
  (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
5864
7144
  )
@@ -5898,15 +7178,19 @@ function buildTable2(schema, table, built, enums, byName, introspection) {
5898
7178
  const drizzleTable = schema.table(table.table, columns, (self) => [
5899
7179
  ...buildCompositePrimaryKey(table, primaryKeyColumns2, self),
5900
7180
  ...buildIndexes2(table, self),
7181
+ ...(table.checks ?? []).map((check$1) => check(check$1.name, sql.raw(compileCheckExpression(check$1.expression, table.columns)))),
5901
7182
  ...buildForeignKeys2(table, self, built, byName),
5902
7183
  ...(table.foreignKeys ?? []).map((fk) => {
5903
7184
  const target = fk.target === table.table ? self : built.get(fk.target);
5904
7185
  if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
5905
- return foreignKey({
7186
+ let constraint = foreignKey({
5906
7187
  name: fk.name,
5907
7188
  columns: fk.columns.map((column) => self[column]),
5908
7189
  foreignColumns: fk.references.map((column) => target[column])
5909
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;
5910
7194
  })
5911
7195
  ]);
5912
7196
  return drizzleTable.enableRLS();
@@ -5997,11 +7281,14 @@ function buildForeignKeys2(table, self, built, byName) {
5997
7281
  `enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
5998
7282
  );
5999
7283
  }
6000
- return foreignKey({
7284
+ let constraint = foreignKey({
6001
7285
  name: foreignKeyName(table.table, link.name),
6002
7286
  columns: [sourceColumn],
6003
7287
  foreignColumns: [targetColumn]
6004
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;
6005
7292
  });
6006
7293
  }
6007
7294
  var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
@@ -6122,9 +7409,23 @@ function findTable2(snapshot, schema, table) {
6122
7409
  const tables = snapshot.tables ?? {};
6123
7410
  return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
6124
7411
  }
6125
- async function planMigrationV2({
7412
+ function planMigrationV2(input) {
7413
+ return planWithViewRecreation({ ...input, viewRecreation: "affected" });
7414
+ }
7415
+ async function planCommittedMigrationV2({
6126
7416
  canonical,
6127
- 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
6128
7429
  }) {
6129
7430
  const schema = ormSchemaName(canonical.namespace);
6130
7431
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
@@ -6142,7 +7443,14 @@ async function planMigrationV2({
6142
7443
  currentViews,
6143
7444
  previousDependencies,
6144
7445
  currentDependencies,
6145
- 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
+ })
6146
7454
  });
6147
7455
  const dropOrder = topologicalViewNames(
6148
7456
  new Set([...affected].filter((name) => previousViews.has(name))),
@@ -6175,7 +7483,12 @@ async function planMigrationV2({
6175
7483
  const view = currentViewDefinitions.get(name);
6176
7484
  if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
6177
7485
  return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
6178
- })
7486
+ }),
7487
+ ...refreshFunctionStatements({
7488
+ namespace: canonical.namespace,
7489
+ previous: previous._sequenceOrm?.refreshFunctions ?? {},
7490
+ current: materializedRefreshFunctions(canonical)
7491
+ }).map((sql4) => ({ kind: "ddl", sql: sql4 }))
6179
7492
  ];
6180
7493
  return { namespace: canonical.namespace, schema, statements, snapshot };
6181
7494
  }
@@ -6329,20 +7642,102 @@ function withoutViews(snapshot) {
6329
7642
  delete clone._sequenceOrm;
6330
7643
  return clone;
6331
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
+ }
6332
7725
  function affectedViewNames({
6333
7726
  previousViews,
6334
7727
  currentViews,
6335
7728
  previousDependencies,
6336
7729
  currentDependencies,
6337
- baseChanged
7730
+ reach
6338
7731
  }) {
6339
7732
  const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
6340
- if (baseChanged) return allNames;
7733
+ if (reach === "all") return allNames;
7734
+ const dependenciesOf = (name) => /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
6341
7735
  const affected = new Set(
6342
7736
  [...allNames].filter((name) => {
6343
7737
  const before = previousViews.get(name);
6344
7738
  const after = currentViews.get(name);
6345
- 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));
6346
7741
  })
6347
7742
  );
6348
7743
  let grew = true;
@@ -6350,8 +7745,7 @@ function affectedViewNames({
6350
7745
  grew = false;
6351
7746
  for (const name of allNames) {
6352
7747
  if (affected.has(name)) continue;
6353
- const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
6354
- if ([...dependencies].some((dependency) => affected.has(dependency))) {
7748
+ if ([...dependenciesOf(name)].some((dependency) => affected.has(dependency))) {
6355
7749
  affected.add(name);
6356
7750
  grew = true;
6357
7751
  }
@@ -7266,7 +8660,9 @@ var PLATFORM_USER_FIELDS = [
7266
8660
  ];
7267
8661
  function generateSdl(canonical, options = {}) {
7268
8662
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
7269
- 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) }));
7270
8666
  if (tables.length === 0) {
7271
8667
  throw new SdlGenerationError(
7272
8668
  `Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
@@ -7325,7 +8721,9 @@ function generateSdl(canonical, options = {}) {
7325
8721
  blocks.push(
7326
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'
7327
8723
  );
8724
+ if (tables.some((table) => table.resolved !== void 0)) blocks.push(...RESOLVED_DIRECTIVE_BLOCKS);
7328
8725
  for (const scalar2 of CUSTOM_SCALARS) blocks.push(`scalar ${scalar2}`);
8726
+ blocks.push(...renderSharedCellTypes([...cellsByTable.values()]));
7329
8727
  blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
7330
8728
  blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
7331
8729
  for (const enumName of [...usedEnums].sort()) {
@@ -7350,7 +8748,8 @@ function generateSdl(canonical, options = {}) {
7350
8748
  names,
7351
8749
  namesByTable,
7352
8750
  emitAggregateBoolExp: aggregateBoolExpTargets.has(table.table),
7353
- pinned: options.pinnedColumns?.[table.table] ?? []
8751
+ pinned: options.pinnedColumns?.[table.table] ?? [],
8752
+ cells: cellsByTable.get(table.table) ?? null
7354
8753
  })
7355
8754
  );
7356
8755
  }
@@ -7440,6 +8839,52 @@ ${orderFields}
7440
8839
  }`
7441
8840
  ];
7442
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
+ }
7443
8888
  function relationArgs(target) {
7444
8889
  return `where: ${target.boolExp}, orderBy: [${target.orderBy}!], limit: Int, offset: Int`;
7445
8890
  }
@@ -7448,10 +8893,10 @@ function rootArgs(names) {
7448
8893
  }
7449
8894
  function primaryKeyColumns(table) {
7450
8895
  if (!table.primaryKey || table.primaryKey.length === 0) return [];
7451
- return table.primaryKey.map((identifier2) => {
7452
- 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);
7453
8898
  if (!column) {
7454
- 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}".`);
7455
8900
  }
7456
8901
  return column;
7457
8902
  });
@@ -7460,6 +8905,7 @@ function renderTableBlocks(ctx) {
7460
8905
  const { table, names } = ctx;
7461
8906
  const blocks = [
7462
8907
  renderObjectType(ctx),
8908
+ ...renderCellsType(ctx),
7463
8909
  renderSelectColumnEnum(ctx),
7464
8910
  renderOrderByInput(ctx),
7465
8911
  renderBoolExp(ctx),
@@ -7474,11 +8920,49 @@ function renderTableBlocks(ctx) {
7474
8920
  ${fields}
7475
8921
  }`);
7476
8922
  }
7477
- if (allowsInsert(table) || allowsUpdate(table)) {
8923
+ if (ctx.table.resolved !== void 0) {
8924
+ blocks.push(...renderResolvedWriteBlocks(ctx));
8925
+ } else if (allowsInsert(table) || allowsUpdate(table)) {
7478
8926
  blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
7479
8927
  }
7480
8928
  return blocks;
7481
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
+ }
7482
8966
  function allowsInsert(table) {
7483
8967
  return table.policies.write !== void 0 || table.policies.insert !== void 0;
7484
8968
  }
@@ -7501,6 +8985,10 @@ function renderObjectType(ctx) {
7501
8985
  const deprecated = column.deprecated ? " @deprecated" : "";
7502
8986
  lines.push(` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}${bang}${deprecated}`);
7503
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
+ }
7504
8992
  for (const link of table.links) {
7505
8993
  if (link.description !== void 0) lines.push(...descriptionLines(link.description, " "));
7506
8994
  if (link.kind === "toPlatformUser") {
@@ -7569,7 +9057,10 @@ function numericishColumns(table) {
7569
9057
  return table.columns.filter((column) => typeof column.type !== "object" && NUMERICISH.has(column.type));
7570
9058
  }
7571
9059
  function jsonbColumns(table) {
7572
- 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;
7573
9064
  }
7574
9065
  function renderAggregateTypes(ctx) {
7575
9066
  const { table, names } = ctx;
@@ -7653,14 +9144,14 @@ function constraintValues(table) {
7653
9144
  return [...values].sort();
7654
9145
  }
7655
9146
  function clientSettableColumns(table) {
7656
- return table.columns.filter((column) => column.onUpdateFn === void 0);
9147
+ return clientWritableColumns(table).filter((column) => column.onUpdateFn === void 0);
7657
9148
  }
7658
9149
  function renderWriteBlocks(ctx, access) {
7659
9150
  const { table, names, pinned } = ctx;
7660
9151
  const blocks = [];
7661
9152
  if (access.insert) {
7662
9153
  const pinnedSet = new Set(pinned);
7663
- const insertable = table.columns.filter(
9154
+ const insertable = clientWritableColumns(table).filter(
7664
9155
  (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
7665
9156
  );
7666
9157
  if (insertable.length === 0) {
@@ -7753,6 +9244,10 @@ function renderMutationRoot(tables, namesByTable) {
7753
9244
  if (!canInsert && !canUpdate) continue;
7754
9245
  const names = namesByTable.get(table.table);
7755
9246
  if (!names) continue;
9247
+ if (table.resolved !== void 0) {
9248
+ lines.push(...renderResolvedMutationFields(table, names));
9249
+ continue;
9250
+ }
7756
9251
  const constraints = constraintValues(table);
7757
9252
  const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
7758
9253
  const updateArgs = [`_set: ${names.setInput}`];
@@ -7841,13 +9336,65 @@ function renderOrmMigrationExport(exported) {
7841
9336
  return text4;
7842
9337
  }
7843
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
+ }
9390
+
7844
9391
  // src/cli.ts
7845
9392
  var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
7846
9393
  var TOKEN_ENV = "SEQUENCE_ORM_TOKEN";
7847
9394
  var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
7848
9395
  var USAGE = `usage:
7849
9396
  seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
7850
- 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)
7851
9398
  seq-studio orm generate [dir] --migration-export [--check]
7852
9399
  export reviewed ORM inputs for a coordinated pipeline migration; --check verifies without writing
7853
9400
  seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
@@ -8018,6 +9565,7 @@ async function walkFiles(root, prefix = "") {
8018
9565
  var SCHEMA_GRAPHQL_FILE = "schema.graphql";
8019
9566
  var OPERATIONS_MANIFEST_FILE = "operations.manifest.json";
8020
9567
  var TYPE_POLICIES_FILE = "typePolicies.gen.ts";
9568
+ var SOURCE_CONTRACTS_FILE = "sources.gen.ts";
8021
9569
  var GRAPHQL_DIRNAME = "graphql";
8022
9570
  var CODEGEN_CONFIG_FILE = "codegen.ts";
8023
9571
  function ormLoadOptions() {
@@ -8037,9 +9585,10 @@ async function compileV2Project(dir) {
8037
9585
  const documents = await collectGraphqlDocuments(dir);
8038
9586
  const result = compileNamespace({ canonical: loaded.canonical, sdl, documents });
8039
9587
  if (!result.ok) return { ok: false, diagnostics: result.diagnostics };
9588
+ const diagnostics = [...result.diagnostics, ...roleWarnings({ canonical: loaded.canonical, manifest: result.manifest })];
8040
9589
  return {
8041
9590
  ok: true,
8042
- project: { loaded, sdl, documents, manifest: result.manifest, diagnostics: result.diagnostics }
9591
+ project: { loaded, sdl, documents, manifest: result.manifest, diagnostics }
8043
9592
  };
8044
9593
  }
8045
9594
  var GQL_TEMPLATE_RE = /(?:\bgql|\/\*\s*GraphQL\s*\*\/)\s*`([^`]*)`/g;
@@ -8143,6 +9692,8 @@ async function writeGenerateOutputs(dir, project) {
8143
9692
  await writeFile(join(dir, OPERATIONS_MANIFEST_FILE), `${JSON.stringify(manifestFile, null, 2)}
8144
9693
  `);
8145
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);
8146
9697
  }
8147
9698
  async function runConsumerCodegen(dir) {
8148
9699
  const codegenPath = join(dir, CODEGEN_CONFIG_FILE);
@@ -8410,16 +9961,28 @@ async function checkWorkspaceV2({
8410
9961
  return 1;
8411
9962
  }
8412
9963
  const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
8413
- const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
8414
- const regenerated = renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan });
8415
9964
  const committed = await readMigrationSql(dir, tip);
8416
- if (regenerated !== committed && !matchesRebaseTip({ committed, tip, namespace, plan })) {
9965
+ const { reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
9966
+ if (!reproduced) {
8417
9967
  log(`${MIGRATIONS_DIRNAME}/${tip.name}.sql does not regenerate from the definitions \u2014 re-run \`seq-studio orm diff\``);
8418
9968
  return 1;
8419
9969
  }
8420
9970
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
8421
9971
  return 0;
8422
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
+ }
8423
9986
  function matchesRebaseTip({
8424
9987
  committed,
8425
9988
  tip,
@@ -8522,7 +10085,6 @@ async function loadCommittedTipMigrationV2({
8522
10085
  loaded,
8523
10086
  workspace
8524
10087
  }) {
8525
- const namespace = loaded.canonical.namespace;
8526
10088
  const tip = workspace.journal.entries.at(-1);
8527
10089
  if (!tip) return void 0;
8528
10090
  if (tip.content_hash !== loaded.hash) {
@@ -8532,10 +10094,9 @@ async function loadCommittedTipMigrationV2({
8532
10094
  );
8533
10095
  }
8534
10096
  const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
8535
- const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
8536
10097
  const committed = await readMigrationSql(dir, tip);
8537
- const regenerated = renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan });
8538
- if (regenerated !== committed && !matchesRebaseTip({ committed, tip, namespace, plan })) {
10098
+ const { plan, reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
10099
+ if (!reproduced) {
8539
10100
  throw new OrmDefinitionError(
8540
10101
  `${tip.name}.sql does not regenerate from the definitions \u2014 run \`seq-studio orm diff --check\``,
8541
10102
  MIGRATIONS_DIRNAME