@sequenceholdings/orm 2.1.0 → 2.2.0

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 (77) hide show
  1. package/README.md +40 -1
  2. package/dist/authoring.cjs +1619 -121
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +252 -8
  5. package/dist/authoring.d.ts +252 -8
  6. package/dist/authoring.js +1607 -123
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +713 -41
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +1520 -51
  11. package/dist/canonical.d.ts +1520 -51
  12. package/dist/canonical.js +693 -42
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +7103 -3901
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +7104 -3902
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs +122 -40
  19. package/dist/client.cjs.map +1 -1
  20. package/dist/client.d.cts +85 -25
  21. package/dist/client.d.ts +85 -25
  22. package/dist/client.js +119 -41
  23. package/dist/client.js.map +1 -1
  24. package/dist/compiler.cjs +2464 -450
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +79 -4
  27. package/dist/compiler.d.ts +79 -4
  28. package/dist/compiler.js +2457 -451
  29. package/dist/compiler.js.map +1 -1
  30. package/dist/constants-C6EG6Wq7.d.cts +99 -0
  31. package/dist/constants-C6EG6Wq7.d.ts +99 -0
  32. package/dist/definition-DjfC7Lll.d.cts +91 -0
  33. package/dist/definition-DjfC7Lll.d.ts +91 -0
  34. package/dist/dynamic-input.cjs +2 -1
  35. package/dist/dynamic-input.cjs.map +1 -1
  36. package/dist/dynamic-input.d.cts +9 -1
  37. package/dist/dynamic-input.d.ts +9 -1
  38. package/dist/dynamic-input.js +2 -1
  39. package/dist/dynamic-input.js.map +1 -1
  40. package/dist/index.cjs +2656 -418
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +693 -116
  43. package/dist/index.d.ts +693 -116
  44. package/dist/index.js +2622 -420
  45. package/dist/index.js.map +1 -1
  46. package/dist/ir.cjs +811 -48
  47. package/dist/ir.cjs.map +1 -1
  48. package/dist/ir.d.cts +670 -27
  49. package/dist/ir.d.ts +670 -27
  50. package/dist/ir.js +802 -49
  51. package/dist/ir.js.map +1 -1
  52. package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
  53. package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
  54. package/dist/policy-1iZbGnZZ.d.cts +24 -0
  55. package/dist/policy-1iZbGnZZ.d.ts +24 -0
  56. package/dist/predicate-v2.cjs +804 -51
  57. package/dist/predicate-v2.cjs.map +1 -1
  58. package/dist/predicate-v2.d.cts +2 -1
  59. package/dist/predicate-v2.d.ts +2 -1
  60. package/dist/predicate-v2.js +804 -51
  61. package/dist/predicate-v2.js.map +1 -1
  62. package/dist/sdl.cjs +1014 -75
  63. package/dist/sdl.cjs.map +1 -1
  64. package/dist/sdl.d.cts +14 -3
  65. package/dist/sdl.d.ts +14 -3
  66. package/dist/sdl.js +1012 -76
  67. package/dist/sdl.js.map +1 -1
  68. package/package.json +13 -7
  69. package/src/codegen-gql/server-fns.cjs +2 -2
  70. package/templates/orm-namespace/AGENTS.md +10 -0
  71. package/templates/orm-namespace/CLAUDE.md +1 -82
  72. package/templates/orm-namespace/codegen.preset.d.cts +7 -0
  73. package/templates/orm-namespace/codegen.ts +2 -1
  74. package/templates/orm-namespace/graphql/example/queries.ts +4 -3
  75. package/templates/orm-namespace/package.json +2 -1
  76. package/templates/orm-namespace/schema/tables.ts +3 -2
  77. package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/compiler.js CHANGED
@@ -1,14 +1,32 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { z } from 'zod';
3
3
  import { Kind, print, visit, buildSchema, parse, validate, valueFromASTUntyped } from 'graphql';
4
+ import { PgParser } from '@supabase/pg-parser';
4
5
 
5
6
  // src/compiler-gql/artifact.ts
6
7
 
7
8
  // src/constants.ts
8
9
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
9
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
10
+ var MAX_IDENTIFIER_LENGTH = 63;
11
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
12
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
10
13
  var ORM_SCHEMA_PREFIX = "orm_";
11
14
  var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
15
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
16
+ var CELL_PART_SUFFIX = {
17
+ at: "__at",
18
+ by: "__by",
19
+ rev: "__rev",
20
+ source: "__src",
21
+ conflicts: "__lost"
22
+ };
23
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
24
+ var CELLS_FIELD = "cells";
25
+ function cellColumnName(column, part) {
26
+ return `${column}${CELL_PART_SUFFIX[part]}`;
27
+ }
28
+ var MATERIALIZE_ACTOR = "orm:materialize";
29
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
12
30
 
13
31
  // src/hash.ts
14
32
  function stableJson(value) {
@@ -17,9 +35,478 @@ function stableJson(value) {
17
35
  const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
18
36
  return `{${entries.join(",")}}`;
19
37
  }
38
+
39
+ // src/compiler/sql.ts
40
+ var CLAIMS_SETTING = "request.jwt.claims";
41
+ var CAPABILITIES_CLAIM_KEY = "capabilities";
42
+ var ROLES_CLAIM_KEY = "roles";
43
+ var quoteIdentifier = (name2) => `"${name2.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 scopedClaimsSource = (namespace) => namespace ? `(CASE WHEN ${CLAIMS_SOURCE} ? 'namespaces' THEN ${CLAIMS_SOURCE} -> 'namespaces' -> ${quoteLiteral(namespace)} ELSE ${CLAIMS_SOURCE} END)` : CLAIMS_SOURCE;
48
+ var capabilityExpression = (key, namespace) => `coalesce(${scopedClaimsSource(namespace)} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
49
+ var roleExpression = (key, namespace) => `coalesce(${scopedClaimsSource(namespace)} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
50
+
51
+ // src/checks/compile.ts
52
+ function tokenize(source) {
53
+ const tokens = [];
54
+ for (let position = 0; position < source.length; ) {
55
+ const char = source[position];
56
+ if (/\s/.test(char)) {
57
+ position++;
58
+ continue;
59
+ }
60
+ if (char === "(" || char === ")" || char === ",") {
61
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
62
+ position++;
63
+ continue;
64
+ }
65
+ if (char === "'") {
66
+ const start = position++;
67
+ let value = "";
68
+ let closed = false;
69
+ while (position < source.length) {
70
+ if (source[position] !== "'") {
71
+ value += source[position++];
72
+ continue;
73
+ }
74
+ if (source[position + 1] === "'") {
75
+ value += "'";
76
+ position += 2;
77
+ continue;
78
+ }
79
+ position++;
80
+ closed = true;
81
+ break;
82
+ }
83
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
84
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
85
+ tokens.push({ kind: "string", value, position: start });
86
+ continue;
87
+ }
88
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
89
+ if (operator) {
90
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
91
+ position += operator[0].length;
92
+ continue;
93
+ }
94
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
95
+ if (number) {
96
+ tokens.push({ kind: "number", value: number[0], position });
97
+ position += number[0].length;
98
+ continue;
99
+ }
100
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
101
+ if (identifier3) {
102
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
103
+ position += identifier3[0].length;
104
+ continue;
105
+ }
106
+ throw new Error(`unexpected character '${char}' at position ${position}`);
107
+ }
108
+ tokens.push({ kind: "end", value: "", position: source.length });
109
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
110
+ return tokens;
111
+ }
112
+ var Parser = class {
113
+ constructor(tokens, columns) {
114
+ this.tokens = tokens;
115
+ this.columns = /* @__PURE__ */ new Map();
116
+ for (const column of columns) {
117
+ for (const alias of [column.name, column.columnName]) {
118
+ const previous = this.columns.get(alias);
119
+ if (previous && previous.columnName !== column.columnName) {
120
+ throw new Error(`ambiguous check column '${alias}'`);
121
+ }
122
+ this.columns.set(alias, column);
123
+ }
124
+ }
125
+ }
126
+ index = 0;
127
+ depth = 0;
128
+ columns;
129
+ parse() {
130
+ const expression = this.parseOr();
131
+ this.expect("end", "end of expression");
132
+ return expression;
133
+ }
134
+ parseOr() {
135
+ let left = this.parseAnd();
136
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
137
+ return left;
138
+ }
139
+ parseAnd() {
140
+ let left = this.parseNot();
141
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
142
+ return left;
143
+ }
144
+ parseNot() {
145
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
146
+ return this.parsePrimary();
147
+ }
148
+ parsePrimary() {
149
+ if (this.peek().kind === "lparen") {
150
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
151
+ this.advance();
152
+ const expression = this.parseOr();
153
+ this.expect("rparen", "')'");
154
+ this.depth--;
155
+ return `(${expression})`;
156
+ }
157
+ return this.parseComparison();
158
+ }
159
+ parseComparison() {
160
+ const left = this.parseValue();
161
+ if (this.matchKeyword("is")) {
162
+ const negate = this.matchKeyword("not");
163
+ this.expectKeyword("null");
164
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
165
+ }
166
+ if (this.matchKeyword("between")) {
167
+ const lower = this.parseValue();
168
+ this.expectKeyword("and");
169
+ const upper = this.parseValue();
170
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
171
+ }
172
+ if (this.matchKeyword("in")) {
173
+ this.expect("lparen", "'(' after IN");
174
+ const values = [this.parseValue()];
175
+ while (this.peek().kind === "comma") {
176
+ this.advance();
177
+ values.push(this.parseValue());
178
+ }
179
+ this.expect("rparen", "')' after IN values");
180
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
181
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
182
+ }
183
+ const operator = this.expect("operator", "a comparison operator");
184
+ const right = this.parseValue();
185
+ if (operator.value === "~" || operator.value === "!~") {
186
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
187
+ if (!right.literal) throw new Error("regex pattern must be a literal");
188
+ }
189
+ return `${left.sql} ${operator.value} ${right.sql}`;
190
+ }
191
+ parseValue() {
192
+ const token = this.advance();
193
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
194
+ if (token.kind === "number") {
195
+ const number = Number(token.value);
196
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
197
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
198
+ }
199
+ return { sql: token.value, literal: true };
200
+ }
201
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
202
+ const keyword = token.value.toLowerCase();
203
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
204
+ if (keyword === "btrim") {
205
+ this.expect("lparen", "'(' after btrim");
206
+ const column2 = this.parseColumn();
207
+ this.expect("rparen", "')' after btrim column");
208
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
209
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
210
+ }
211
+ const column = this.columns.get(token.value);
212
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
213
+ return { sql: quoteIdentifier(column.columnName), column };
214
+ }
215
+ parseColumn() {
216
+ const token = this.expect("identifier", "a column name");
217
+ const column = this.columns.get(token.value);
218
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
219
+ return column;
220
+ }
221
+ peek() {
222
+ return this.tokens[this.index];
223
+ }
224
+ advance() {
225
+ const token = this.peek();
226
+ if (token.kind !== "end") this.index++;
227
+ return token;
228
+ }
229
+ matchKeyword(keyword) {
230
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
231
+ this.advance();
232
+ return true;
233
+ }
234
+ expectKeyword(keyword) {
235
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
236
+ }
237
+ expect(kind, description) {
238
+ const token = this.peek();
239
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
240
+ return this.advance();
241
+ }
242
+ };
243
+ function compileCheckExpression(source, columns) {
244
+ return new Parser(tokenize(source), columns).parse();
245
+ }
246
+ var policyVariantSchema = z.object({
247
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
248
+ when: z.string().min(1).max(1e3),
249
+ /** Exactly one of: allow-all bypass, or a row predicate. */
250
+ allow: z.literal(true).optional(),
251
+ policy: z.string().min(1).max(2e3).optional()
252
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
253
+ message: "A variant declares exactly one of allow: true or policy"
254
+ });
255
+ var policyExpressionSchema = z.union([
256
+ z.string().min(1).max(2e3),
257
+ z.object({
258
+ default: z.string().min(1).max(2e3),
259
+ variants: z.array(policyVariantSchema).max(10).default([])
260
+ })
261
+ ]);
262
+ function parseDurationMs(input) {
263
+ if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
264
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
265
+ if (!match) return void 0;
266
+ const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
267
+ const value = Math.round(Number(match[1]) * factor);
268
+ return value > 0 ? value : void 0;
269
+ }
270
+ var identifier = z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
271
+ var durationMs = z.number().int().positive().max(24 * 60 * 60 * 1e3);
272
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
273
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
274
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
275
+ };
276
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
277
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
278
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
279
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
280
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
281
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
282
+ };
283
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
284
+ /* @__PURE__ */ new Set([
285
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
286
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
287
+ ]);
288
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
289
+ var canonicalFunctionRefSchema = z.object({
290
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
291
+ kind: z.enum(["managed", "native"]),
292
+ id: z.string().min(1).max(200)
293
+ }).strict();
294
+ var canonicalFetchShapeSchema = z.union([
295
+ z.object({ shape: z.literal("single") }).strict(),
296
+ z.object({ shape: z.literal("batch"), maxKeys: z.number().int().min(2).max(500) }).strict()
297
+ ]);
298
+ var canonicalResolvedSourceSchema = z.object({
299
+ name: identifier,
300
+ kind: z.enum(["manual", "etl", "prefetch"]),
301
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
302
+ table: identifier,
303
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
304
+ columns: z.array(z.string().min(1).max(63)).min(1).max(500),
305
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
306
+ updatedAt: z.string().min(1).max(63).optional(),
307
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
308
+ key: z.string().min(1).max(63).optional(),
309
+ fetch: canonicalFetchShapeSchema.optional(),
310
+ function: canonicalFunctionRefSchema.optional(),
311
+ prefetchDefaults: z.object({
312
+ maxStalenessMs: durationMs,
313
+ timeoutMs: durationMs,
314
+ onError: z.enum(["fallback", "fail"])
315
+ }).strict().optional(),
316
+ writeback: z.object({
317
+ columns: z.array(z.string().min(1).max(63)).min(1).max(500),
318
+ defaultMode: z.enum(["sync", "async"]),
319
+ timeoutMs: durationMs,
320
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
321
+ clearManualOnConfirm: z.boolean()
322
+ }).strict().optional()
323
+ }).strict();
324
+ var canonicalResolveRuleSchema = z.object({
325
+ column: z.string().min(1).max(63),
326
+ comparator: z.enum(COMPARATORS),
327
+ /** Source names, in tie-break order. */
328
+ candidates: z.array(identifier).min(1).max(16),
329
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
330
+ orderBy: z.string().min(1).max(2e3).optional()
331
+ }).strict();
332
+ var canonicalResolvedSchema = z.object({
333
+ sources: z.array(canonicalResolvedSourceSchema).min(1).max(16),
334
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
335
+ resolve: z.array(canonicalResolveRuleSchema).max(500),
336
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
337
+ deletedBy: z.array(identifier).max(1).default([]),
338
+ /**
339
+ * Materialized: the view reads an indexed internal table (`table`) that the
340
+ * data plane keeps in step with `compute`, the internal view holding the
341
+ * resolution query. Omitted when the view computes on read.
342
+ */
343
+ materialized: z.object({ table: identifier, compute: identifier }).strict().optional(),
344
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
345
+ policies: z.object({
346
+ write: policyExpressionSchema.optional(),
347
+ insert: policyExpressionSchema.optional(),
348
+ update: policyExpressionSchema.optional()
349
+ }).strict().default({})
350
+ }).strict();
351
+ function validateResolvedView({ view, tables, views = [] }) {
352
+ const resolved = view.resolved;
353
+ if (!resolved) return [];
354
+ const errors = [];
355
+ const fail4 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
356
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
357
+ if (!pkColumn) {
358
+ fail4("requires exactly one primary-key column");
359
+ return errors;
360
+ }
361
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
362
+ const scope = {
363
+ view,
364
+ resolved,
365
+ pkColumn,
366
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
367
+ sourceByName: /* @__PURE__ */ new Map(),
368
+ fail: fail4
369
+ };
370
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail4("requires exactly one manual source");
371
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
372
+ for (const source of resolved.sources) {
373
+ if (scope.sourceByName.has(source.name)) fail4(`duplicate source '${source.name}'`);
374
+ scope.sourceByName.set(source.name, source);
375
+ const table = tableByName.get(source.table);
376
+ if (!table) fail4(`source '${source.name}' references unknown table '${source.table}'`);
377
+ else validateSource({ scope, source, table });
378
+ }
379
+ validateRules(scope);
380
+ validateViewCells(scope);
381
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
382
+ for (const name2 of resolved.deletedBy) {
383
+ if (scope.sourceByName.get(name2)?.kind !== "manual") fail4(`deletedBy '${name2}' must be the manual source`);
384
+ }
385
+ return errors;
386
+ }
387
+ function validateSource({ scope, source, table }) {
388
+ const { fail: fail4 } = scope;
389
+ if (source.kind === "etl" && table.source === void 0) fail4(`etl source '${source.name}' must reference a synced table`);
390
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
391
+ fail4(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
392
+ }
393
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
394
+ if (source.kind === "manual" && !hasRev) fail4(`manual source '${source.name}' must carry '__rev' cell columns`);
395
+ if (source.kind === "prefetch" && hasRev) fail4(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
396
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
397
+ validateSourceColumns({ scope, source, byName });
398
+ if (source.kind === "etl") {
399
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
400
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
401
+ fail4(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
402
+ }
403
+ }
404
+ validatePrefetchFields({ scope, source });
405
+ }
406
+ function validatePrefetchFields({ scope, source }) {
407
+ const { fail: fail4, pkColumn } = scope;
408
+ if (source.kind !== "prefetch") {
409
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
410
+ fail4(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
411
+ }
412
+ return;
413
+ }
414
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
415
+ fail4(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
416
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
417
+ fail4(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
418
+ }
419
+ for (const column of source.writeback?.columns ?? []) {
420
+ if (!source.columns.includes(column)) fail4(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
421
+ }
422
+ }
423
+ function validateSourceColumns({ scope, source, byName }) {
424
+ const { fail: fail4, pkColumn, dataColumns } = scope;
425
+ for (const column of [pkColumn.name, ...source.columns]) {
426
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
427
+ const theirs = byName.get(column);
428
+ if (!own) fail4(`source '${source.name}' supplies unknown column '${column}'`);
429
+ else if (!theirs) fail4(`source '${source.name}' table '${source.table}' has no column '${column}'`);
430
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail4(`column '${column}' has a different type on source '${source.name}'`);
431
+ }
432
+ }
433
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail4 }) {
434
+ const ruled = /* @__PURE__ */ new Set();
435
+ for (const rule of resolved.resolve) {
436
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
437
+ fail4(`resolve rule targets unknown column '${rule.column}'`);
438
+ continue;
439
+ }
440
+ if (ruled.has(rule.column)) fail4(`column '${rule.column}' has more than one resolve rule`);
441
+ ruled.add(rule.column);
442
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
443
+ fail4(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
444
+ }
445
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail4(`column '${rule.column}' repeats a candidate`);
446
+ for (const candidate of rule.candidates) {
447
+ const source = sourceByName.get(candidate);
448
+ if (!source) fail4(`column '${rule.column}' names unknown candidate source '${candidate}'`);
449
+ else if (!source.columns.includes(rule.column)) fail4(`column '${rule.column}' candidate '${candidate}' does not supply it`);
450
+ }
451
+ }
452
+ for (const column of dataColumns.keys()) {
453
+ if (column !== pkColumn.name && !ruled.has(column)) fail4(`column '${column}' has no resolve rule`);
454
+ }
455
+ }
456
+ var VIEW_CELL_TYPES = {
457
+ source: "text",
458
+ at: "timestamptz",
459
+ by: "text",
460
+ conflicts: "jsonb"
461
+ };
462
+ function validateViewCells({ view, dataColumns, pkColumn, fail: fail4 }) {
463
+ const expected = /* @__PURE__ */ new Map();
464
+ for (const column of dataColumns.values()) {
465
+ if (column.name === pkColumn.name) continue;
466
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
467
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
468
+ }
469
+ }
470
+ for (const column of view.columns) {
471
+ if (!column.cell) continue;
472
+ const want = expected.get(column.name);
473
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
474
+ fail4(`unexpected cell column '${column.name}'`);
475
+ continue;
476
+ }
477
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail4(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
478
+ expected.delete(column.name);
479
+ }
480
+ for (const name2 of expected.keys()) fail4(`missing cell column '${name2}'`);
481
+ }
482
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
483
+ function validateMaterialized({ scope, tableByName, views }) {
484
+ const { view, resolved, fail: fail4 } = scope;
485
+ const { table: tableName, compute: computeName } = resolved.materialized;
486
+ const table = tableByName.get(tableName);
487
+ if (!table || table.internal !== true || table.materializes !== view.view) {
488
+ fail4(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
489
+ } else {
490
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail4(`materialized table '${tableName}' must have exactly the view's columns`);
491
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail4(`materialized table '${tableName}' must share the view's primary key`);
492
+ }
493
+ const compute = views.find((candidate) => candidate.view === computeName);
494
+ if (!compute || compute.internal !== true) fail4(`compute view '${computeName}' must be an internal view`);
495
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail4(`compute view '${computeName}' must have exactly the view's columns`);
496
+ for (const source of resolved.sources) {
497
+ if (source.kind !== "etl") continue;
498
+ const read = tableByName.get(source.table)?.policies?.read;
499
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
500
+ if (!unconditional) {
501
+ fail4(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
502
+ }
503
+ }
504
+ }
505
+
506
+ // src/canonical/v2.ts
20
507
  var CANONICAL_FORMAT = 2;
21
508
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
22
- var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
509
+ var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
23
510
  var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
24
511
  var COLUMN_TYPES = [
25
512
  "uuid",
@@ -35,14 +522,14 @@ var COLUMN_TYPES = [
35
522
  ];
36
523
  var columnTypeSchema = z.enum(COLUMN_TYPES);
37
524
  var canonicalEnumSchema = z.object({
38
- name: identifier,
525
+ name: identifier2,
39
526
  values: z.array(z.string().min(1).max(63)).min(1).max(100)
40
527
  });
41
528
  var canonicalColumnSchema = z.object({
42
529
  /** Property name in TS (camelCase); columnName is the Postgres name. */
43
530
  name: z.string().min(1).max(63),
44
531
  columnName: columnIdentifier,
45
- type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
532
+ type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
46
533
  nullable: z.boolean().default(true),
47
534
  unique: z.boolean().default(false),
48
535
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -58,7 +545,14 @@ var canonicalColumnSchema = z.object({
58
545
  /** Migration hints — same semantics as v1. */
59
546
  renamedFrom: columnIdentifier.optional(),
60
547
  cast: z.string().max(200).optional(),
61
- deprecated: z.boolean().default(false)
548
+ deprecated: z.boolean().default(false),
549
+ /**
550
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
551
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
552
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
553
+ * ordinary columns so their hashes are unchanged.
554
+ */
555
+ cell: z.object({ of: z.string().min(1).max(63), part: z.enum(CELL_PARTS) }).strict().optional()
62
556
  }).superRefine((column, ctx) => {
63
557
  if (column.onUpdateFn === void 0) return;
64
558
  if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
@@ -73,36 +567,28 @@ var canonicalLinkSchema = z.object({
73
567
  name: z.string().min(1).max(63),
74
568
  kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
75
569
  /** Target table name (namespace-local). Absent for toPlatformUser. */
76
- target: identifier.optional(),
570
+ target: identifier2.optional(),
571
+ /** Owning namespace of an imported target; omitted for local links. */
572
+ namespace: identifier2.max(41).optional(),
77
573
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
78
574
  on: columnIdentifier,
79
575
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
80
576
  references: columnIdentifier.optional(),
81
577
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
82
578
  enforce: z.boolean().default(false),
579
+ onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
580
+ onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
83
581
  description: z.string().max(500).optional()
84
582
  });
85
583
  var canonicalIndexSchema = z.object({
86
584
  columns: z.array(columnIdentifier).min(1),
87
585
  unique: z.boolean().default(false),
88
- name: identifier.optional()
89
- });
90
- var policyVariantSchema = z.object({
91
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
92
- when: z.string().min(1).max(1e3),
93
- /** Exactly one of: allow-all bypass, or a row predicate. */
94
- allow: z.literal(true).optional(),
95
- policy: z.string().min(1).max(2e3).optional()
96
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
97
- message: "A variant declares exactly one of allow: true or policy"
586
+ name: identifier2.optional()
98
587
  });
99
- var policyExpressionSchema = z.union([
100
- z.string().min(1).max(2e3),
101
- z.object({
102
- default: z.string().min(1).max(2e3),
103
- variants: z.array(policyVariantSchema).max(10).default([])
104
- })
105
- ]);
588
+ var canonicalCheckSchema = z.object({
589
+ name: identifier2,
590
+ expression: z.string().min(1).max(2e3)
591
+ }).strict();
106
592
  var canonicalPoliciesSchema = z.object({
107
593
  read: policyExpressionSchema.optional(),
108
594
  write: policyExpressionSchema.optional(),
@@ -133,7 +619,7 @@ function isSingleViewQuerySql(value) {
133
619
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
134
620
  }
135
621
  var canonicalActionSchema = z.object({
136
- name: identifier,
622
+ name: identifier2,
137
623
  requires: z.string().max(1e3).optional(),
138
624
  params: z.record(z.string(), z.object({
139
625
  type: columnTypeSchema,
@@ -145,9 +631,9 @@ var canonicalActionSchema = z.object({
145
631
  });
146
632
  var canonicalCowMutationSchema = z.object({
147
633
  kind: z.enum(["override", "revert"]),
148
- name: identifier,
634
+ name: identifier2,
149
635
  /** Synced table (same namespace) the overlay copies from / falls back to. */
150
- synced: identifier,
636
+ synced: identifier2,
151
637
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
152
638
  pk: z.string().min(1).max(63),
153
639
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -175,23 +661,28 @@ var canonicalCowMutationSchema = z.object({
175
661
  }
176
662
  });
177
663
  var canonicalForeignKeySchema = z.object({
178
- name: identifier,
664
+ name: identifier2,
179
665
  columns: z.array(columnIdentifier).min(1).max(8),
180
- target: identifier,
666
+ target: identifier2,
181
667
  references: z.array(columnIdentifier).min(1).max(8),
182
- deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional()
668
+ deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional(),
669
+ onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
670
+ onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
183
671
  }).strict();
184
672
  var canonicalTableSchema = z.object({
185
- table: identifier,
673
+ table: identifier2,
674
+ displayName: z.string().trim().min(1).max(120).optional(),
186
675
  description: z.string().max(2e3).optional(),
187
676
  /** Table-level rename hint (new in v2). */
188
- renamedFrom: identifier.optional(),
189
- /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
190
- source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
677
+ renamedFrom: identifier2.optional(),
678
+ /** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
679
+ source: z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
191
680
  columns: z.array(canonicalColumnSchema).default([]),
192
681
  primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
193
682
  links: z.array(canonicalLinkSchema).default([]),
194
683
  indexes: z.array(canonicalIndexSchema).default([]),
684
+ /** Omitted by old definitions, preserving their existing content hashes. */
685
+ checks: z.array(canonicalCheckSchema).max(100).optional(),
195
686
  policies: canonicalPoliciesSchema.default({}),
196
687
  /**
197
688
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -202,22 +693,55 @@ var canonicalTableSchema = z.object({
202
693
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
203
694
  cowMutations: z.array(canonicalCowMutationSchema).optional(),
204
695
  /** Omitted by old definitions, preserving their existing content hashes. */
205
- foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional()
696
+ foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional(),
697
+ /**
698
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
699
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
700
+ * on a resolved table's internal sources), listed explicitly in `columns`
701
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
702
+ * Omitted when off so existing definitions hash unchanged.
703
+ */
704
+ cellMetadata: z.literal(true).optional(),
705
+ /**
706
+ * System-owned backing table of a resolved table (its manual or prefetch
707
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
708
+ * only the data plane writes them. Omitted when false.
709
+ */
710
+ internal: z.literal(true).optional(),
711
+ /**
712
+ * The materialized copy of resolved table `materializes` (internal): the
713
+ * same columns as that view, including its cell parts, maintained by the
714
+ * data plane. Omitted otherwise.
715
+ */
716
+ materializes: identifier2.optional()
206
717
  });
718
+ var MAX_VIEW_QUERY = 1e5;
719
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
207
720
  var canonicalViewSchema = z.object({
208
- view: identifier,
721
+ view: identifier2,
209
722
  description: z.string().max(2e3).optional(),
210
723
  columns: z.array(canonicalColumnSchema).min(1),
211
724
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
212
725
  primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
213
726
  links: z.array(canonicalLinkSchema).default([]),
214
727
  policies: z.object({ read: policyExpressionSchema }).strict(),
215
- query: z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
728
+ query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
216
729
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
217
730
  }),
218
731
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
219
- dependencies: z.array(identifier).max(500)
732
+ dependencies: z.array(identifier2).max(500),
733
+ /**
734
+ * Present when this view is a resolved table (defineResolvedTable): the
735
+ * multi-source metadata behind its generated query. Omitted otherwise so
736
+ * existing views hash unchanged.
737
+ */
738
+ resolved: canonicalResolvedSchema.optional(),
739
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
740
+ internal: z.literal(true).optional()
220
741
  }).superRefine((view, ctx) => {
742
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
743
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
744
+ }
221
745
  for (const [index, column] of view.columns.entries()) {
222
746
  if (column.default !== void 0 || column.defaultFn !== void 0) {
223
747
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
@@ -232,20 +756,91 @@ var canonicalViewSchema = z.object({
232
756
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
233
757
  }
234
758
  }
759
+ if (view.resolved === void 0 && view.internal !== true) {
760
+ for (const [index, column] of view.columns.entries()) {
761
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
762
+ }
763
+ }
235
764
  for (const [index, link] of view.links.entries()) {
236
765
  if (link.enforce) {
237
766
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
238
767
  }
768
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
769
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
770
+ }
239
771
  }
240
772
  });
241
- var canonicalDefinitionSchema = z.object({
773
+ var CELL_PART_TYPES = {
774
+ at: "timestamptz",
775
+ by: "text",
776
+ rev: "uuid",
777
+ source: "text",
778
+ conflicts: "jsonb"
779
+ };
780
+ function trackedCellColumns(table) {
781
+ const pk = new Set(table.primaryKey ?? []);
782
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
783
+ }
784
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
785
+ function validateCellMetadata(table, index, ctx) {
786
+ const fail4 = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
787
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
788
+ if (table.materializes !== void 0) return;
789
+ if (table.cellMetadata !== true) {
790
+ for (const { position } of cells) fail4(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
791
+ return;
792
+ }
793
+ if (table.source !== void 0) {
794
+ fail4(`synced table '${table.table}' cannot track cell metadata`);
795
+ return;
796
+ }
797
+ const tracked = trackedCellColumns(table);
798
+ const byName = new Map(tracked.map((column) => [column.name, column]));
799
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
800
+ if (withRev && table.internal !== true) fail4(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
801
+ const seen = /* @__PURE__ */ new Set();
802
+ for (const { column, position, cell } of cells) {
803
+ const path = ["tables", index, "columns", position];
804
+ const owner = byName.get(cell.of);
805
+ if (!owner) {
806
+ fail4(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
807
+ continue;
808
+ }
809
+ if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail4(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
810
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
811
+ fail4(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
812
+ }
813
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
814
+ fail4(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
815
+ }
816
+ const key = `${cell.of}:${cell.part}`;
817
+ if (seen.has(key)) fail4(`duplicate cell column '${column.name}'`, path);
818
+ seen.add(key);
819
+ }
820
+ for (const column of tracked) {
821
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
822
+ fail4(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
823
+ }
824
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
825
+ fail4(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
826
+ }
827
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
828
+ if (!seen.has(`${column.name}:${part}`)) fail4(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
829
+ }
830
+ }
831
+ }
832
+ var exportGateSchema = z.union([z.literal(true), z.object({ capability: z.string().min(1) }).strict(), z.object({ role: z.string().min(1) }).strict()]);
833
+ var relationExportSchema = z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
834
+ var localDefinitionSchema = z.object({
242
835
  definition_format: z.literal(CANONICAL_FORMAT),
243
- namespace: identifier.max(41),
836
+ namespace: identifier2.max(41),
244
837
  description: z.string().max(2e3).optional(),
245
838
  enums: z.array(canonicalEnumSchema).default([]),
246
839
  tables: z.array(canonicalTableSchema).min(0).max(500),
247
840
  /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
248
841
  views: z.array(canonicalViewSchema).max(500).optional(),
842
+ /** Target-owned cross-namespace access contract, using existing namespace grants. */
843
+ exports: z.record(identifier2, relationExportSchema).optional(),
249
844
  /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
250
845
  capabilities: z.object({
251
846
  roles: z.unknown().optional(),
@@ -258,6 +853,7 @@ var canonicalDefinitionSchema = z.object({
258
853
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
259
854
  }
260
855
  seen.add(table.table);
856
+ validateCellMetadata(table, index, ctx);
261
857
  if (table.source !== void 0) {
262
858
  for (const [columnIndex, column] of table.columns.entries()) {
263
859
  if (column.onUpdateFn !== void 0) {
@@ -271,31 +867,68 @@ var canonicalDefinitionSchema = z.object({
271
867
  }
272
868
  }
273
869
  const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
870
+ const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
871
+ for (const name2 of Object.keys(definition.exports ?? {})) {
872
+ if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
873
+ }
274
874
  for (const [tableIndex, table] of definition.tables.entries()) {
875
+ for (const [linkIndex, link] of table.links.entries()) {
876
+ if (link.enforce && link.namespace !== void 0 && link.namespace !== definition.namespace) {
877
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", linkIndex], message: "physical foreign keys require a local target; use an ORM link without enforce" });
878
+ }
879
+ }
275
880
  const names = /* @__PURE__ */ new Set([
276
881
  ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
277
882
  ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
278
883
  ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
279
884
  ]);
885
+ for (const [index, link] of table.links.entries()) {
886
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
887
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
888
+ }
889
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
890
+ const source = table.columns.find((column) => column.columnName === link.on);
891
+ if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "SET NULL requires a nullable source column" });
892
+ }
893
+ }
894
+ for (const [index, check] of (table.checks ?? []).entries()) {
895
+ const fail4 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
896
+ if (table.source) fail4("synced tables cannot declare checks");
897
+ if (names.has(check.name)) fail4(`duplicate constraint '${check.name}'`);
898
+ names.add(check.name);
899
+ try {
900
+ compileCheckExpression(check.expression, table.columns);
901
+ } catch (error) {
902
+ fail4(error instanceof Error ? error.message : String(error));
903
+ }
904
+ }
280
905
  for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
281
- const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
282
- if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
906
+ const fail4 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
907
+ if (names.has(fk.name)) fail4(`duplicate foreign key '${fk.name}'`);
283
908
  names.add(fk.name);
284
909
  const target = fkTables.get(fk.target);
285
910
  if (table.source || !target || target.source) {
286
- fail2("foreign keys must connect owned tables in the same namespace");
911
+ fail4("foreign keys must connect owned tables in the same namespace");
287
912
  continue;
288
913
  }
289
- if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
290
- if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
291
- for (const [position, name] of fk.columns.entries()) {
292
- const sourceColumn = table.columns.find((column) => column.columnName === name);
914
+ if (fk.columns.length !== fk.references.length) fail4("foreign key columns and references must have the same number of columns");
915
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail4("foreign key columns must not repeat");
916
+ for (const [position, name2] of fk.columns.entries()) {
917
+ const sourceColumn = table.columns.find((column) => column.columnName === name2);
293
918
  const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
294
- if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
295
- else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
919
+ if (!sourceColumn || !targetColumn) fail4("foreign key references an unknown column");
920
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail4("foreign key column types must match");
921
+ }
922
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
923
+ if (fk.columns.some((name2) => table.columns.find((column) => column.columnName === name2)?.nullable === false || table.primaryKey?.includes(name2))) fail4("SET NULL requires nullable source columns");
296
924
  }
297
925
  const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
298
- 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");
926
+ if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail4("foreign key target columns must form a unique key");
927
+ }
928
+ }
929
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
930
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
931
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
299
932
  }
300
933
  }
301
934
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
@@ -392,27 +1025,31 @@ var canonicalDefinitionSchema = z.object({
392
1025
  const visiting = /* @__PURE__ */ new Set();
393
1026
  const visited = /* @__PURE__ */ new Set();
394
1027
  let cycleReported = false;
395
- const visit2 = (name, path) => {
396
- if (visited.has(name) || cycleReported) return;
397
- if (visiting.has(name)) {
1028
+ const visit2 = (name2, path) => {
1029
+ if (visited.has(name2) || cycleReported) return;
1030
+ if (visiting.has(name2)) {
398
1031
  cycleReported = true;
399
1032
  ctx.addIssue({
400
1033
  code: "custom",
401
1034
  path: ["views"],
402
- message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
1035
+ message: `view dependencies form a cycle: ${[...path, name2].join(" -> ")}`
403
1036
  });
404
1037
  return;
405
1038
  }
406
- visiting.add(name);
407
- const view = viewByName.get(name);
1039
+ visiting.add(name2);
1040
+ const view = viewByName.get(name2);
408
1041
  for (const dependency of view?.dependencies ?? []) {
409
- if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
1042
+ if (viewByName.has(dependency)) visit2(dependency, [...path, name2]);
410
1043
  }
411
- visiting.delete(name);
412
- visited.add(name);
1044
+ visiting.delete(name2);
1045
+ visited.add(name2);
413
1046
  };
414
1047
  for (const view of views) visit2(view.view, []);
415
1048
  });
1049
+ var canonicalDefinitionSchema = localDefinitionSchema.safeExtend({
1050
+ /** Flattened, pinned snapshots. Imported relations never participate in this namespace's DDL. */
1051
+ imports: z.array(localDefinitionSchema).max(32).optional()
1052
+ });
416
1053
  function canonicalRelations(definition) {
417
1054
  const views = (definition.views ?? []).map((view) => ({
418
1055
  table: view.view,
@@ -421,8 +1058,12 @@ function canonicalRelations(definition) {
421
1058
  ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
422
1059
  links: view.links,
423
1060
  indexes: [],
424
- policies: { read: view.policies.read },
425
- actions: []
1061
+ // A resolved table's write policies (evaluated against the resolved row)
1062
+ // ride alongside the view's read policy.
1063
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
1064
+ actions: [],
1065
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
1066
+ ...view.internal ? { internal: true } : {}
426
1067
  }));
427
1068
  return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
428
1069
  }
@@ -432,13 +1073,14 @@ function normalizeCanonical(definition) {
432
1073
  ...definition,
433
1074
  enums: sortByName(definition.enums),
434
1075
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
435
- const { cowMutations, foreignKeys, ...rest } = t;
1076
+ const { cowMutations, foreignKeys, checks, ...rest } = t;
436
1077
  return {
437
1078
  ...rest,
438
1079
  links: sortByName(t.links),
439
1080
  indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
440
1081
  actions: sortByName(t.actions),
441
1082
  ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1083
+ ...checks?.length ? { checks: sortByName(checks) } : {},
442
1084
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
443
1085
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
444
1086
  };
@@ -451,13 +1093,22 @@ function normalizeCanonical(definition) {
451
1093
  dependencies: [...view.dependencies].sort()
452
1094
  }));
453
1095
  }
1096
+ if (definition.imports?.length) {
1097
+ normalized.imports = definition.imports.map(normalizeCanonical).sort((a, b) => a.namespace.localeCompare(b.namespace));
1098
+ } else {
1099
+ delete normalized.imports;
1100
+ }
454
1101
  return normalized;
455
1102
  }
456
1103
 
457
1104
  // src/ir/index.ts
458
- var IR_VERSION = 2;
1105
+ var IR_VERSION = 5;
459
1106
  var LEGACY_IR_VERSION = 1;
460
- var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(IR_VERSION)]);
1107
+ var GUARDED_IR_VERSION = 2;
1108
+ var COMPOSED_IR_VERSION = 3;
1109
+ var SQL_IR_VERSION = 4;
1110
+ var RUNTIME_SQL_IR_VERSION = 5;
1111
+ var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(GUARDED_IR_VERSION), z.literal(COMPOSED_IR_VERSION), z.literal(SQL_IR_VERSION), z.literal(RUNTIME_SQL_IR_VERSION)]);
461
1112
  var operationVersionSchema = z.object({ irVersion: irVersionSchema });
462
1113
  function requiredArtifactIrVersion(operations) {
463
1114
  let required = LEGACY_IR_VERSION;
@@ -509,8 +1160,8 @@ var predIrSchema = z.lazy(
509
1160
  value: z.number(),
510
1161
  where: predIrSchema.optional()
511
1162
  }),
512
- z.object({ kind: z.literal("capability"), key: z.string() }),
513
- z.object({ kind: z.literal("role"), key: z.string() }),
1163
+ z.object({ kind: z.literal("capability"), key: z.string(), namespace: z.string().optional() }),
1164
+ z.object({ kind: z.literal("role"), key: z.string(), namespace: z.string().optional() }),
514
1165
  z.object({ kind: z.literal("dynamicFilter"), slot: z.string() }),
515
1166
  z.object({ kind: z.literal("const"), value: z.boolean() })
516
1167
  ])
@@ -538,6 +1189,21 @@ var selectionIrSchema = z.lazy(
538
1189
  () => z.object({
539
1190
  typename: z.string(),
540
1191
  columns: z.array(z.object({ alias: z.string(), column: z.string() })),
1192
+ cells: z.object({
1193
+ alias: z.string(),
1194
+ typename: z.string(),
1195
+ fields: z.array(z.object({
1196
+ alias: z.string(),
1197
+ column: z.string(),
1198
+ typename: z.string(),
1199
+ parts: z.array(z.object({
1200
+ alias: z.string(),
1201
+ part: z.enum(["source", "at", "by", "conflicts"]),
1202
+ fields: z.array(z.object({ alias: z.string(), field: z.enum(["source", "at", "value"]) })).optional(),
1203
+ typename: z.string().optional()
1204
+ }))
1205
+ }))
1206
+ }).optional(),
541
1207
  relations: z.array(z.object({
542
1208
  alias: z.string(),
543
1209
  link: z.string(),
@@ -559,6 +1225,11 @@ var selectionIrSchema = z.lazy(
559
1225
  })
560
1226
  );
561
1227
  var includeIfSchema = z.object({ variable: z.string(), negated: z.boolean() });
1228
+ var prefetchPlanIrSchema = z.object({
1229
+ source: z.string(),
1230
+ maxStalenessMs: z.number().int().positive().optional(),
1231
+ key: z.union([z.object({ one: valueIrSchema }), z.object({ many: valueIrSchema })])
1232
+ });
562
1233
  var selectRootSchema = z.object({
563
1234
  kind: z.literal("select"),
564
1235
  alias: z.string(),
@@ -570,7 +1241,8 @@ var selectRootSchema = z.object({
570
1241
  distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
571
1242
  limit: limitIrSchema,
572
1243
  offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
573
- includeIf: includeIfSchema.optional()
1244
+ includeIf: includeIfSchema.optional(),
1245
+ prefetch: z.array(prefetchPlanIrSchema).optional()
574
1246
  });
575
1247
  var byPkRootSchema = z.object({
576
1248
  kind: z.literal("byPk"),
@@ -580,7 +1252,8 @@ var byPkRootSchema = z.object({
580
1252
  pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
581
1253
  /** Policy predicate — ANDed with the PK match. */
582
1254
  where: predIrSchema,
583
- includeIf: includeIfSchema.optional()
1255
+ includeIf: includeIfSchema.optional(),
1256
+ prefetch: z.array(prefetchPlanIrSchema).optional()
584
1257
  });
585
1258
  var aggregateRootSchema = z.object({
586
1259
  kind: z.literal("aggregate"),
@@ -638,7 +1311,20 @@ var mutationRootSchema = z.object({
638
1311
  * coalesce view falls back to synced. Both are gated on a matching synced
639
1312
  * row and carry the write policy in `where` like update/delete.
640
1313
  */
641
- op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
1314
+ op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
1315
+ /**
1316
+ * resolvedWrite: a write to a resolved table (`table` is its view). The
1317
+ * executor gates on the resolved row (write policy in `where`), writes only
1318
+ * the touched cells into the manual source, then re-checks `insertCheck`
1319
+ * against the resolved row. `revert` clears `revertColumns` from the manual
1320
+ * source; `delete` tombstones the row there.
1321
+ */
1322
+ resolvedAction: z.enum(["insert", "update", "delete", "revert"]).optional(),
1323
+ revertColumns: z.array(z.string()).optional(),
1324
+ /** `@writeback(source:, mode:)` directives on this root. */
1325
+ writeback: z.array(z.object({ source: z.string(), mode: z.enum(["sync", "async"]) })).optional(),
1326
+ /** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
1327
+ localOnly: z.array(z.string()).optional(),
642
1328
  alias: z.string(),
643
1329
  table: z.string(),
644
1330
  /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
@@ -702,7 +1388,31 @@ var variableIrSchema = z.object({
702
1388
  /** Absent = no default declared; see {@link variableDefaultIrSchema}. */
703
1389
  defaultValue: variableDefaultIrSchema.optional()
704
1390
  });
1391
+ var sqlColumnSchema = z.object({ name: z.string(), type: z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]), nullable: z.boolean() });
705
1392
  var compiledOperationSchema = z.object({
1393
+ language: z.literal("sql").optional(),
1394
+ /** Server-compiled object catalog and permission ceiling, never caller SQL. */
1395
+ sqlQuery: z.object({
1396
+ catalog: z.object({
1397
+ identity: z.string(),
1398
+ objects: z.array(z.object({
1399
+ ref: z.string(),
1400
+ table: z.string(),
1401
+ columns: z.array(sqlColumnSchema.extend({ type: z.string() })).min(1),
1402
+ scan: z.string().min(1),
1403
+ read: z.array(z.string()).min(1)
1404
+ }).strict()).min(1).max(256)
1405
+ }).strict(),
1406
+ read: z.array(z.string()).min(1).max(256),
1407
+ maxRows: z.number().int().min(1).max(1e4)
1408
+ }).strict().optional(),
1409
+ sql: z.object({
1410
+ text: z.string().min(1).max(1048576),
1411
+ columns: z.array(sqlColumnSchema).min(1).max(256),
1412
+ parameters: z.array(z.string()).max(128),
1413
+ read: z.array(z.string()).min(1).max(256),
1414
+ maxRows: z.number().int().min(1).max(1e4)
1415
+ }).strict().optional(),
706
1416
  irVersion: irVersionSchema,
707
1417
  name: z.string(),
708
1418
  opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
@@ -714,8 +1424,16 @@ var compiledOperationSchema = z.object({
714
1424
  variables: z.array(variableIrSchema),
715
1425
  /** Every table the op touches — data-token scoping + audit. */
716
1426
  tableSet: z.array(z.string()).min(1),
717
- roots: z.array(rootIrSchema).min(1)
1427
+ roots: z.array(rootIrSchema)
718
1428
  }).superRefine((operation, ctx) => {
1429
+ if (operation.language === "sql") {
1430
+ const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
1431
+ if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !operation.requires?.capability) {
1432
+ ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
1433
+ }
1434
+ } else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
1435
+ ctx.addIssue({ code: "custom", message: "GraphQL operations require roots and cannot carry SQL" });
1436
+ }
719
1437
  for (const [index, root] of operation.roots.entries()) {
720
1438
  if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
721
1439
  if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
@@ -723,13 +1441,55 @@ var compiledOperationSchema = z.object({
723
1441
  }
724
1442
  }
725
1443
  });
1444
+ var resolvedSourceIrSchema = z.object({
1445
+ name: z.string(),
1446
+ kind: z.enum(["manual", "etl", "prefetch"]),
1447
+ /** Logical table key of the backing relation. */
1448
+ table: z.string(),
1449
+ columns: z.array(z.string()),
1450
+ key: z.string().optional(),
1451
+ fetch: z.union([
1452
+ z.object({ shape: z.literal("single") }),
1453
+ z.object({ shape: z.literal("batch"), maxKeys: z.number().int().positive() })
1454
+ ]).optional(),
1455
+ function: z.object({ kind: z.enum(["managed", "native"]), id: z.string() }).optional(),
1456
+ prefetchDefaults: z.object({
1457
+ maxStalenessMs: z.number().int().positive(),
1458
+ timeoutMs: z.number().int().positive(),
1459
+ onError: z.enum(["fallback", "fail"])
1460
+ }).optional(),
1461
+ writeback: z.object({
1462
+ columns: z.array(z.string()),
1463
+ defaultMode: z.enum(["sync", "async"]),
1464
+ timeoutMs: z.number().int().positive(),
1465
+ clearManualOnConfirm: z.boolean()
1466
+ }).optional()
1467
+ });
1468
+ var resolvedMetaIrSchema = z.object({
1469
+ /** Logical pk column of the resolved view (single-column). */
1470
+ pk: z.string(),
1471
+ /** Source name of the manual source. */
1472
+ manual: z.string(),
1473
+ sources: z.array(resolvedSourceIrSchema),
1474
+ deletedBy: z.array(z.string()),
1475
+ /**
1476
+ * Materialized: the view reads `table`, which the data plane refreshes from
1477
+ * the `compute` view (claims-gated) whenever it writes a source row.
1478
+ */
1479
+ materialized: z.object({ table: z.string(), compute: z.string() }).optional()
1480
+ });
726
1481
  var tableMetaIrSchema = z.object({
727
1482
  table: z.string(),
1483
+ namespace: z.string().optional(),
1484
+ dependencies: z.array(z.string()).optional(),
1485
+ foreignReferences: z.array(z.object({ column: z.string(), target: z.string(), targetColumn: z.string() })).optional(),
1486
+ exportRead: z.union([z.literal(true), z.object({ capability: z.string() }), z.object({ role: z.string() })]).optional(),
1487
+ exportWrite: z.union([z.literal(true), z.object({ capability: z.string() }), z.object({ role: z.string() })]).optional(),
728
1488
  schema: z.string(),
729
1489
  typename: z.string(),
730
1490
  /**
731
1491
  * True when statements over this relation evaluate a claims-gated synced
732
- * view: synced twins (`source: gold.*`) materialize as views whose baked
1492
+ * view: synced twins (`source: schema.table`) materialize as views whose baked
733
1493
  * policy reads `current_setting('request.jwt.claims')`, and namespace views
734
1494
  * inherit the gate transitively through their declared dependencies. The
735
1495
  * executor must stamp that GUC (transaction-scoped) before any statement
@@ -763,6 +1523,22 @@ var tableMetaIrSchema = z.object({
763
1523
  * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
764
1524
  */
765
1525
  touchOnUpdate: z.array(z.string()).optional(),
1526
+ /**
1527
+ * Per-cell metadata: logical column → the logical names of its cell-part
1528
+ * columns (which are listed in `columns`, but never filterable, orderable or
1529
+ * writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
1530
+ * write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
1531
+ * the relation tracks no cells.
1532
+ */
1533
+ cells: z.record(z.string(), z.object({
1534
+ at: z.string().optional(),
1535
+ by: z.string().optional(),
1536
+ rev: z.string().optional(),
1537
+ source: z.string().optional(),
1538
+ conflicts: z.string().optional()
1539
+ }).strict()).optional(),
1540
+ /** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
1541
+ resolved: resolvedMetaIrSchema.optional(),
766
1542
  constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
767
1543
  /** Depth/node caps for dynamic filter trees on this table. */
768
1544
  dynamicFilterMaxDepth: z.number().int().positive().default(6),
@@ -772,13 +1548,21 @@ var compiledArtifactSchema = z.object({
772
1548
  irVersion: irVersionSchema,
773
1549
  namespace: z.string(),
774
1550
  schemaHash: z.string(),
1551
+ ownSchemaHash: z.string().optional(),
1552
+ dependencies: z.record(z.string(), z.string()).optional(),
1553
+ readPolicies: z.record(z.string(), predIrSchema).optional(),
775
1554
  opsHash: z.string(),
1555
+ /** Apply-observed synced objects without a serving backing; not part of the authored schema hash. */
1556
+ unbackedTables: z.array(z.string()).optional(),
776
1557
  tables: z.record(z.string(), tableMetaIrSchema),
777
1558
  operations: z.record(z.string(), compiledOperationSchema)
778
1559
  }).superRefine((artifact, ctx) => {
779
- for (const [name, operation] of Object.entries(artifact.operations)) {
1560
+ if (Object.keys(artifact.dependencies ?? {}).length > 0 && artifact.irVersion < COMPOSED_IR_VERSION) {
1561
+ ctx.addIssue({ code: "custom", path: ["irVersion"], message: "cross-namespace dependencies require IR v3" });
1562
+ }
1563
+ for (const [name2, operation] of Object.entries(artifact.operations)) {
780
1564
  if (operation.irVersion > artifact.irVersion) {
781
- ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
1565
+ ctx.addIssue({ code: "custom", path: ["operations", name2, "irVersion"], message: "operation requires a newer IR version than its artifact" });
782
1566
  }
783
1567
  }
784
1568
  });
@@ -791,10 +1575,119 @@ var IrVersionError = class extends Error {
791
1575
  function normalizeIr(raw) {
792
1576
  if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
793
1577
  const version = raw.irVersion;
794
- if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
1578
+ if (!irVersionSchema.safeParse(version).success) throw new IrVersionError(version);
795
1579
  return compiledArtifactSchema.parse(raw);
796
1580
  }
797
1581
 
1582
+ // src/sdl/naming.ts
1583
+ function camelCase(name2) {
1584
+ const segments = name2.split("_").filter((segment) => segment.length > 0);
1585
+ const first = segments[0];
1586
+ if (first === void 0) return name2;
1587
+ const head = first.charAt(0).toLowerCase() + first.slice(1);
1588
+ return head + segments.slice(1).map(capitalizeFirst).join("");
1589
+ }
1590
+ function pascalCase(name2) {
1591
+ return capitalizeFirst(camelCase(name2));
1592
+ }
1593
+ function capitalizeFirst(segment) {
1594
+ return segment.charAt(0).toUpperCase() + segment.slice(1);
1595
+ }
1596
+ function singularizeWord(word) {
1597
+ if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
1598
+ for (const suffix of ["sses", "xes", "ches", "shes"]) {
1599
+ if (word.endsWith(suffix)) return word.slice(0, -2);
1600
+ }
1601
+ if (word.endsWith("ss")) return word;
1602
+ if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
1603
+ return word;
1604
+ }
1605
+ function typeName(table) {
1606
+ const segments = table.split("_").filter((segment) => segment.length > 0);
1607
+ const last = segments[segments.length - 1];
1608
+ if (last === void 0) return pascalCase(table);
1609
+ return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
1610
+ }
1611
+ function enumTypeName(name2) {
1612
+ return pascalCase(name2);
1613
+ }
1614
+ var SCALAR_FOR_COLUMN_TYPE = {
1615
+ uuid: "uuid",
1616
+ text: "String",
1617
+ integer: "Int",
1618
+ bigint: "bigint",
1619
+ numeric: "numeric",
1620
+ double: "Float",
1621
+ boolean: "Boolean",
1622
+ timestamptz: "timestamptz",
1623
+ date: "date",
1624
+ jsonb: "jsonb"
1625
+ };
1626
+ function graphqlScalarFor(type) {
1627
+ return SCALAR_FOR_COLUMN_TYPE[type];
1628
+ }
1629
+ var COLUMN_TYPE_BY_SCALAR = new Map([
1630
+ ["ID", "text"],
1631
+ ...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
1632
+ ]);
1633
+ function columnTypeForScalar(scalarName) {
1634
+ return COLUMN_TYPE_BY_SCALAR.get(scalarName);
1635
+ }
1636
+ var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
1637
+ var PLATFORM_USER_TYPENAME = "PlatformUser";
1638
+ function tableNames(table) {
1639
+ const root = camelCase(table);
1640
+ const rootPascal = pascalCase(table);
1641
+ const type = typeName(table);
1642
+ return {
1643
+ typename: type,
1644
+ selectRoot: root,
1645
+ byPkRoot: `${root}ByPk`,
1646
+ aggregateRoot: `${root}Aggregate`,
1647
+ insertRoot: `insert${rootPascal}`,
1648
+ insertOneRoot: `insert${rootPascal}One`,
1649
+ updateRoot: `update${rootPascal}`,
1650
+ updateByPkRoot: `update${rootPascal}ByPk`,
1651
+ deleteRoot: `delete${rootPascal}`,
1652
+ deleteByPkRoot: `delete${rootPascal}ByPk`,
1653
+ revertByPkRoot: `revert${rootPascal}ByPk`,
1654
+ boolExp: `${type}BoolExp`,
1655
+ orderBy: `${type}OrderBy`,
1656
+ selectColumn: `${type}SelectColumn`,
1657
+ insertInput: `${type}InsertInput`,
1658
+ setInput: `${type}SetInput`,
1659
+ appendInput: `${type}AppendInput`,
1660
+ incInput: `${type}IncInput`,
1661
+ onConflict: `${type}OnConflict`,
1662
+ pkColumnsInput: `${type}PkColumnsInput`,
1663
+ constraintEnum: `${type}Constraint`,
1664
+ updateColumnEnum: `${type}UpdateColumn`,
1665
+ mutationResponse: `${type}MutationResponse`,
1666
+ aggregate: `${type}Aggregate`,
1667
+ aggregateFields: `${type}AggregateFields`,
1668
+ sumFields: `${type}SumFields`,
1669
+ avgFields: `${type}AvgFields`,
1670
+ aggregateBoolExp: `${type}AggregateBoolExp`,
1671
+ aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
1672
+ aggregateGroupsRoot: `${root}AggregateGroups`,
1673
+ groupByColumnEnum: `${type}GroupByColumn`,
1674
+ groupByInput: `${type}GroupBy`,
1675
+ groupKey: `${type}GroupKey`,
1676
+ aggregateGroup: `${type}AggregateGroup`
1677
+ };
1678
+ }
1679
+ var CELL_TYPENAMES = {
1680
+ /** A plain table's cell: when and by whom it was last written. */
1681
+ stamp: "CellStamp",
1682
+ /** A resolved table's cell: which source won, when, by whom, and newer losers. */
1683
+ resolved: "ResolvedCell",
1684
+ /** A newer candidate that lost to the column's rule. */
1685
+ conflict: "CellConflict"
1686
+ };
1687
+ function cellsTypename(typename) {
1688
+ return `${typename}Cells`;
1689
+ }
1690
+
798
1691
  // src/predicate-v2/ast.ts
799
1692
  var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
800
1693
  function walkPolicy(node, visit2) {
@@ -831,7 +1724,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
831
1724
  var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
832
1725
  var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
833
1726
  var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
834
- function tokenize(source) {
1727
+ function tokenize2(source) {
835
1728
  const tokens = [];
836
1729
  let position = 0;
837
1730
  while (position < source.length) {
@@ -910,7 +1803,7 @@ function readString(source, start) {
910
1803
  }
911
1804
  throw new PolicyParseError("unterminated string literal", start);
912
1805
  }
913
- var Parser = class {
1806
+ var Parser2 = class {
914
1807
  constructor(tokens) {
915
1808
  this.tokens = tokens;
916
1809
  }
@@ -1084,7 +1977,7 @@ var Parser = class {
1084
1977
  function parsePolicy(source) {
1085
1978
  const trimmed = source.trim();
1086
1979
  if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
1087
- return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
1980
+ return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
1088
1981
  }
1089
1982
 
1090
1983
  // src/predicate-v2/compile.ts
@@ -1484,6 +2377,7 @@ var PolicyInjector = class {
1484
2377
  injectMutation(root) {
1485
2378
  const table = root.table;
1486
2379
  const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
2380
+ if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
1487
2381
  if (root.op === "insert" || root.op === "insertOne") {
1488
2382
  const insertPolicy = this.insert(table);
1489
2383
  if (predContainsExists(insertPolicy)) {
@@ -1506,7 +2400,7 @@ var PolicyInjector = class {
1506
2400
  const updatePolicy = this.update(table);
1507
2401
  const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
1508
2402
  if (root.op === "update" || root.op === "updateByPk") {
1509
- const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
2403
+ const check = predContainsExists(updatePolicy) && !this.ctx.composed ? {} : { insertCheck: updatePolicy };
1510
2404
  return { ...root, where, ...check, returning };
1511
2405
  }
1512
2406
  if (root.op === "cowOverride") {
@@ -1520,11 +2414,29 @@ var PolicyInjector = class {
1520
2414
  const pins = (this.ctx.pins.get(table) ?? []).map(
1521
2415
  (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
1522
2416
  );
1523
- const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
2417
+ const check = predContainsExists(updatePolicy) && !this.ctx.composed ? {} : { insertCheck: updatePolicy };
1524
2418
  return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
1525
2419
  }
1526
2420
  return { ...root, where, returning };
1527
2421
  }
2422
+ /**
2423
+ * Resolved writes are policed against the RESOLVED row, not the sparse
2424
+ * manual row: `where` gates the existing row (update/revert/delete) and
2425
+ * `insertCheck` is re-evaluated as a SELECT over the view after the write,
2426
+ * so link-traversing (exists) policies are representable here.
2427
+ */
2428
+ injectResolvedWrite(root, returning) {
2429
+ const table = root.table;
2430
+ if (root.resolvedAction === "insert") {
2431
+ const pins = (this.ctx.pins.get(table) ?? []).map(
2432
+ (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
2433
+ );
2434
+ return { ...root, pinnedColumns: pins, insertCheck: this.insert(table), returning };
2435
+ }
2436
+ const updatePolicy = this.update(table);
2437
+ const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
2438
+ return root.resolvedAction === "delete" ? { ...root, where, returning } : { ...root, where, insertCheck: updatePolicy, returning };
2439
+ }
1528
2440
  };
1529
2441
  function collectTableSet(roots, ctx) {
1530
2442
  const tables = /* @__PURE__ */ new Set();
@@ -1571,136 +2483,40 @@ function collectTableSet(roots, ctx) {
1571
2483
  tables.add(root.table);
1572
2484
  switch (root.kind) {
1573
2485
  case "select":
1574
- case "byPk":
1575
- visitPred(root.where, root.table);
1576
- visitSelection(root.selection, root.table);
1577
- break;
1578
- case "aggregate":
1579
- visitPred(root.where, root.table);
1580
- if (root.nodes) visitSelection(root.nodes, root.table);
1581
- break;
1582
- case "aggregateGroups":
1583
- visitPred(root.where, root.table);
1584
- break;
1585
- case "mutation":
1586
- if (root.cowSynced !== void 0) tables.add(root.cowSynced);
1587
- if (root.where) visitPred(root.where, root.table);
1588
- if (root.insertCheck) visitPred(root.insertCheck, root.table);
1589
- if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
1590
- if (root.returning) visitSelection(root.returning, root.table);
1591
- break;
1592
- default: {
1593
- const exhausted = root;
1594
- throw new DocumentCompileError(
1595
- `internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
1596
- "internal"
1597
- );
1598
- }
1599
- }
1600
- }
1601
- return [...tables].sort();
1602
- }
1603
- function injectPolicies({ roots, ctx }) {
1604
- const injector = new PolicyInjector(ctx);
1605
- const injected = roots.map((root) => injector.injectRoot(root));
1606
- return { roots: injected, tableSet: collectTableSet(injected, ctx) };
1607
- }
1608
-
1609
- // src/sdl/naming.ts
1610
- function camelCase(name) {
1611
- const segments = name.split("_").filter((segment) => segment.length > 0);
1612
- const first = segments[0];
1613
- if (first === void 0) return name;
1614
- const head = first.charAt(0).toLowerCase() + first.slice(1);
1615
- return head + segments.slice(1).map(capitalizeFirst).join("");
1616
- }
1617
- function pascalCase(name) {
1618
- return capitalizeFirst(camelCase(name));
1619
- }
1620
- function capitalizeFirst(segment) {
1621
- return segment.charAt(0).toUpperCase() + segment.slice(1);
1622
- }
1623
- function singularizeWord(word) {
1624
- if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
1625
- for (const suffix of ["sses", "xes", "ches", "shes"]) {
1626
- if (word.endsWith(suffix)) return word.slice(0, -2);
1627
- }
1628
- if (word.endsWith("ss")) return word;
1629
- if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
1630
- return word;
1631
- }
1632
- function typeName(table) {
1633
- const segments = table.split("_").filter((segment) => segment.length > 0);
1634
- const last = segments[segments.length - 1];
1635
- if (last === void 0) return pascalCase(table);
1636
- return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
1637
- }
1638
- function enumTypeName(name) {
1639
- return pascalCase(name);
1640
- }
1641
- var SCALAR_FOR_COLUMN_TYPE = {
1642
- uuid: "uuid",
1643
- text: "String",
1644
- integer: "Int",
1645
- bigint: "bigint",
1646
- numeric: "numeric",
1647
- double: "Float",
1648
- boolean: "Boolean",
1649
- timestamptz: "timestamptz",
1650
- date: "date",
1651
- jsonb: "jsonb"
1652
- };
1653
- function graphqlScalarFor(type) {
1654
- return SCALAR_FOR_COLUMN_TYPE[type];
1655
- }
1656
- var COLUMN_TYPE_BY_SCALAR = new Map([
1657
- ["ID", "text"],
1658
- ...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
1659
- ]);
1660
- function columnTypeForScalar(scalarName) {
1661
- return COLUMN_TYPE_BY_SCALAR.get(scalarName);
1662
- }
1663
- var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
1664
- var PLATFORM_USER_TYPENAME = "PlatformUser";
1665
- function tableNames(table) {
1666
- const root = camelCase(table);
1667
- const rootPascal = pascalCase(table);
1668
- const type = typeName(table);
1669
- return {
1670
- typename: type,
1671
- selectRoot: root,
1672
- byPkRoot: `${root}ByPk`,
1673
- aggregateRoot: `${root}Aggregate`,
1674
- insertRoot: `insert${rootPascal}`,
1675
- insertOneRoot: `insert${rootPascal}One`,
1676
- updateRoot: `update${rootPascal}`,
1677
- updateByPkRoot: `update${rootPascal}ByPk`,
1678
- deleteRoot: `delete${rootPascal}`,
1679
- deleteByPkRoot: `delete${rootPascal}ByPk`,
1680
- boolExp: `${type}BoolExp`,
1681
- orderBy: `${type}OrderBy`,
1682
- selectColumn: `${type}SelectColumn`,
1683
- insertInput: `${type}InsertInput`,
1684
- setInput: `${type}SetInput`,
1685
- appendInput: `${type}AppendInput`,
1686
- incInput: `${type}IncInput`,
1687
- onConflict: `${type}OnConflict`,
1688
- pkColumnsInput: `${type}PkColumnsInput`,
1689
- constraintEnum: `${type}Constraint`,
1690
- updateColumnEnum: `${type}UpdateColumn`,
1691
- mutationResponse: `${type}MutationResponse`,
1692
- aggregate: `${type}Aggregate`,
1693
- aggregateFields: `${type}AggregateFields`,
1694
- sumFields: `${type}SumFields`,
1695
- avgFields: `${type}AvgFields`,
1696
- aggregateBoolExp: `${type}AggregateBoolExp`,
1697
- aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
1698
- aggregateGroupsRoot: `${root}AggregateGroups`,
1699
- groupByColumnEnum: `${type}GroupByColumn`,
1700
- groupByInput: `${type}GroupBy`,
1701
- groupKey: `${type}GroupKey`,
1702
- aggregateGroup: `${type}AggregateGroup`
1703
- };
2486
+ case "byPk":
2487
+ visitPred(root.where, root.table);
2488
+ visitSelection(root.selection, root.table);
2489
+ break;
2490
+ case "aggregate":
2491
+ visitPred(root.where, root.table);
2492
+ if (root.nodes) visitSelection(root.nodes, root.table);
2493
+ break;
2494
+ case "aggregateGroups":
2495
+ visitPred(root.where, root.table);
2496
+ break;
2497
+ case "mutation":
2498
+ if (root.cowSynced !== void 0) tables.add(root.cowSynced);
2499
+ for (const source of ctx.tables[root.table]?.resolved?.sources ?? []) tables.add(source.table);
2500
+ if (root.where) visitPred(root.where, root.table);
2501
+ if (root.insertCheck) visitPred(root.insertCheck, root.table);
2502
+ if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
2503
+ if (root.returning) visitSelection(root.returning, root.table);
2504
+ break;
2505
+ default: {
2506
+ const exhausted = root;
2507
+ throw new DocumentCompileError(
2508
+ `internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
2509
+ "internal"
2510
+ );
2511
+ }
2512
+ }
2513
+ }
2514
+ return [...tables].sort();
2515
+ }
2516
+ function injectPolicies({ roots, ctx }) {
2517
+ const injector = new PolicyInjector(ctx);
2518
+ const injected = roots.map((root) => injector.injectRoot(root));
2519
+ return { roots: injected, tableSet: collectTableSet(injected, ctx) };
1704
2520
  }
1705
2521
 
1706
2522
  // src/compiler-gql/naming.ts
@@ -1749,16 +2565,92 @@ var NAME_KEY_BY_KIND = {
1749
2565
  function inputTypeName(table, kind) {
1750
2566
  return tableNames(table)[NAME_KEY_BY_KIND[kind]];
1751
2567
  }
1752
- function classifyInputType(name, tableByTypename) {
2568
+ function classifyInputType(name2, tableByTypename) {
1753
2569
  for (const [suffix, input] of INPUT_TYPE_SUFFIXES) {
1754
- if (!name.endsWith(suffix) || name.length <= suffix.length) continue;
1755
- const table = tableByTypename.get(name.slice(0, -suffix.length));
2570
+ if (!name2.endsWith(suffix) || name2.length <= suffix.length) continue;
2571
+ const table = tableByTypename.get(name2.slice(0, -suffix.length));
1756
2572
  if (table !== void 0) return { input, table };
1757
2573
  }
1758
2574
  return void 0;
1759
2575
  }
1760
2576
 
2577
+ // src/errors.ts
2578
+ var OrmDefinitionError = class extends Error {
2579
+ constructor(message, file) {
2580
+ super(file ? `${file}: ${message}` : message);
2581
+ this.file = file;
2582
+ }
2583
+ name = "OrmDefinitionError";
2584
+ };
2585
+
2586
+ // src/canonical/composition.ts
2587
+ function localContract(definition) {
2588
+ return normalizeCanonical(localDefinitionSchema.parse(definition));
2589
+ }
2590
+ function composeNamespace(definition) {
2591
+ const all = [localContract(definition), ...definition.imports ?? []];
2592
+ const owners = /* @__PURE__ */ new Map();
2593
+ const namespaces = /* @__PURE__ */ new Set();
2594
+ const enums = /* @__PURE__ */ new Map();
2595
+ for (const owner of all) {
2596
+ if (namespaces.has(owner.namespace)) throw new OrmDefinitionError(`duplicate imported namespace '${owner.namespace}'`);
2597
+ namespaces.add(owner.namespace);
2598
+ for (const value of owner.enums) {
2599
+ const previous = enums.get(value.name);
2600
+ if (previous && stableJson(previous) !== stableJson(value)) {
2601
+ throw new OrmDefinitionError(`conflicting imported enum '${value.name}'`);
2602
+ }
2603
+ enums.set(value.name, value);
2604
+ }
2605
+ for (const relation of canonicalRelations(owner)) {
2606
+ if (owners.has(relation.table)) throw new OrmDefinitionError(`ambiguous imported relation '${relation.table}'`);
2607
+ owners.set(relation.table, owner);
2608
+ }
2609
+ }
2610
+ for (const owner of definition.imports?.length ? all : []) {
2611
+ for (const relation of canonicalRelations(owner)) {
2612
+ for (const link of relation.links) {
2613
+ if (link.kind === "toPlatformUser") continue;
2614
+ const target = owners.get(link.target ?? "");
2615
+ if (!target || target.namespace !== (link.namespace ?? owner.namespace)) {
2616
+ throw new OrmDefinitionError(`link '${owner.namespace}/${relation.table}.${link.name}' has no matching imported target`);
2617
+ }
2618
+ if (target.namespace !== owner.namespace && !target.exports?.[link.target ?? ""]) {
2619
+ throw new OrmDefinitionError(`target '${target.namespace}/${link.target}' is not exported`);
2620
+ }
2621
+ }
2622
+ }
2623
+ }
2624
+ return {
2625
+ owners,
2626
+ definition: {
2627
+ ...localContract(definition),
2628
+ tables: all.flatMap((item) => item.tables),
2629
+ views: all.flatMap((item) => item.views ?? []),
2630
+ enums: [...enums.values()]
2631
+ }
2632
+ };
2633
+ }
2634
+
1761
2635
  // src/compiler-gql/context.ts
2636
+ function scopePolicy(pred, namespace) {
2637
+ switch (pred.kind) {
2638
+ case "capability":
2639
+ case "role":
2640
+ return { ...pred, namespace };
2641
+ case "and":
2642
+ case "or":
2643
+ return { ...pred, operands: pred.operands.map((p) => scopePolicy(p, namespace)) };
2644
+ case "not":
2645
+ return { ...pred, operand: scopePolicy(pred.operand, namespace) };
2646
+ case "exists":
2647
+ return { ...pred, where: scopePolicy(pred.where, namespace) };
2648
+ case "aggPredicate":
2649
+ return pred.where ? { ...pred, where: scopePolicy(pred.where, namespace) } : pred;
2650
+ default:
2651
+ return pred;
2652
+ }
2653
+ }
1762
2654
  function platformUserMeta() {
1763
2655
  const columns = [
1764
2656
  // `sub` is the view's key (auth0_id, filtered NOT NULL in the view).
@@ -1814,7 +2706,8 @@ function resolveColumnEntries(table, enums, diagnostics) {
1814
2706
  type,
1815
2707
  nullable: column.nullable,
1816
2708
  deprecated: column.deprecated,
1817
- onUpdateFn: column.onUpdateFn !== void 0
2709
+ onUpdateFn: column.onUpdateFn !== void 0,
2710
+ ...column.cell ? { cell: column.cell } : {}
1818
2711
  };
1819
2712
  });
1820
2713
  }
@@ -1822,7 +2715,8 @@ function logicalColumn(entries, ref) {
1822
2715
  return entries.find((c) => c.name === ref || c.columnName === ref)?.name;
1823
2716
  }
1824
2717
  function buildCompileContext(rawCanonical) {
1825
- const canonical = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
2718
+ const normalized = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
2719
+ const { definition: canonical, owners } = composeNamespace(normalized);
1826
2720
  const diagnostics = [];
1827
2721
  const enums = new Map(canonical.enums.map((e) => [e.name, e]));
1828
2722
  const relations = canonicalRelations(canonical);
@@ -1835,24 +2729,25 @@ function buildCompileContext(rawCanonical) {
1835
2729
  const syncedTableNames = new Set(canonical.tables.filter((t) => t.source !== void 0).map((t) => t.table));
1836
2730
  const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
1837
2731
  const claimsGatedMemo = /* @__PURE__ */ new Map();
1838
- const relationIsClaimsGated = (name) => {
1839
- if (syncedTableNames.has(name)) return true;
1840
- const memo = claimsGatedMemo.get(name);
2732
+ const relationIsClaimsGated = (name2) => {
2733
+ if (syncedTableNames.has(name2)) return true;
2734
+ const memo = claimsGatedMemo.get(name2);
1841
2735
  if (memo !== void 0) return memo;
1842
- claimsGatedMemo.set(name, false);
1843
- const gated = (viewDependencies.get(name) ?? []).some(relationIsClaimsGated);
1844
- claimsGatedMemo.set(name, gated);
2736
+ claimsGatedMemo.set(name2, false);
2737
+ const gated = (viewDependencies.get(name2) ?? []).some(relationIsClaimsGated);
2738
+ claimsGatedMemo.set(name2, gated);
1845
2739
  return gated;
1846
2740
  };
1847
2741
  for (const table of relations) {
1848
2742
  const isView = viewNames.has(table.table);
2743
+ const owner = owners.get(table.table);
1849
2744
  const entries = resolveColumnEntries(table, enums, diagnostics);
1850
2745
  columnEntries.set(table.table, entries);
1851
2746
  const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
1852
2747
  const primaryKey = [];
1853
2748
  for (const ref of pkRefs) {
1854
- const resolved = logicalColumn(entries, ref);
1855
- if (!resolved) {
2749
+ const resolved2 = logicalColumn(entries, ref);
2750
+ if (!resolved2) {
1856
2751
  diagnostics.push({
1857
2752
  severity: "error",
1858
2753
  code: "unknown-pk-column",
@@ -1861,9 +2756,12 @@ function buildCompileContext(rawCanonical) {
1861
2756
  });
1862
2757
  continue;
1863
2758
  }
1864
- primaryKey.push(resolved);
2759
+ primaryKey.push(resolved2);
1865
2760
  }
1866
- const active = entries.filter((c) => !c.deprecated);
2761
+ const resolved = isView ? table.resolved : void 0;
2762
+ const data = entries.filter((c) => c.cell === void 0);
2763
+ const cells = cellMap(entries);
2764
+ const active = data.filter((c) => !c.deprecated);
1867
2765
  const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
1868
2766
  const constraints = [];
1869
2767
  if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
@@ -1880,7 +2778,10 @@ function buildCompileContext(rawCanonical) {
1880
2778
  }
1881
2779
  tables[table.table] = {
1882
2780
  table: table.table,
1883
- schema: ormSchemaName(canonical.namespace),
2781
+ schema: ormSchemaName(owner.namespace),
2782
+ namespace: owner.namespace,
2783
+ ...viewDependencies.has(table.table) ? { dependencies: viewDependencies.get(table.table) } : {},
2784
+ ...owner.exports?.[table.table] ? { exportRead: owner.exports[table.table].read, exportWrite: owner.exports[table.table].write } : {},
1884
2785
  typename: typeNameForTable(table.table),
1885
2786
  // Synced twins (and views reading them, resolved transitively below)
1886
2787
  // are claims-gated — see tableMetaIrSchema.claimsGated.
@@ -1888,12 +2789,14 @@ function buildCompileContext(rawCanonical) {
1888
2789
  columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
1889
2790
  primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
1890
2791
  links: [],
1891
- filterable: entries.map((c) => c.name),
1892
- orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
1893
- insertable: isView ? [] : active.map((c) => c.name),
1894
- settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
2792
+ filterable: data.map((c) => c.name),
2793
+ orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
2794
+ insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
2795
+ settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey.includes(name2)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
1895
2796
  appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
1896
2797
  ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
2798
+ ...cells ? { cells } : {},
2799
+ ...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey[0] }) } : {},
1897
2800
  constraints: isView ? [] : constraints,
1898
2801
  dynamicFilterMaxDepth: 6,
1899
2802
  dynamicFilterMaxNodes: 64
@@ -1905,7 +2808,7 @@ function buildCompileContext(rawCanonical) {
1905
2808
  for (const link of table.links) {
1906
2809
  const targetName = link.kind === "toPlatformUser" ? PLATFORM_USER_TABLE : link.target;
1907
2810
  const target = targetName === void 0 ? void 0 : tables[targetName];
1908
- if (!targetName || !target) {
2811
+ if (!targetName || !target || link.namespace !== void 0 && link.namespace !== target.namespace) {
1909
2812
  diagnostics.push({
1910
2813
  severity: "error",
1911
2814
  code: "unknown-link-target",
@@ -1930,6 +2833,11 @@ function buildCompileContext(rawCanonical) {
1930
2833
  continue;
1931
2834
  }
1932
2835
  meta.links.push({ name: link.name, kind: link.kind, target: targetName, on, references });
2836
+ if (link.kind !== "toPlatformUser" && meta.namespace !== target.namespace) {
2837
+ const fkOwner = link.kind === "toMany" ? target : meta;
2838
+ const referenced = link.kind === "toMany" ? table.table : targetName;
2839
+ (fkOwner.foreignReferences ??= []).push({ column: on, target: referenced, targetColumn: references });
2840
+ }
1933
2841
  }
1934
2842
  }
1935
2843
  const readPolicies = /* @__PURE__ */ new Map();
@@ -1947,7 +2855,8 @@ function buildCompileContext(rawCanonical) {
1947
2855
  return;
1948
2856
  }
1949
2857
  try {
1950
- store.set(table.table, compilePolicy({ expression, table: table.table, resolver }));
2858
+ const pred = compilePolicy({ expression, table: table.table, resolver });
2859
+ store.set(table.table, normalized.imports?.length ? scopePolicy(pred, owners.get(table.table).namespace) : pred);
1951
2860
  } catch (error) {
1952
2861
  if (error instanceof PolicyCompileError || error instanceof PolicyParseError) {
1953
2862
  diagnostics.push({
@@ -1966,7 +2875,7 @@ function buildCompileContext(rawCanonical) {
1966
2875
  compilePolicyFor("write", writePolicies);
1967
2876
  compilePolicyFor("insert", insertPolicies);
1968
2877
  compilePolicyFor("update", updatePolicies);
1969
- if (isView) {
2878
+ if (isView && table.resolved === void 0) {
1970
2879
  writePolicies.set(table.table, CONST_FALSE);
1971
2880
  insertPolicies.set(table.table, CONST_FALSE);
1972
2881
  updatePolicies.set(table.table, CONST_FALSE);
@@ -2001,10 +2910,47 @@ function buildCompileContext(rawCanonical) {
2001
2910
  updatePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
2002
2911
  pins.set(PLATFORM_USER_TABLE, []);
2003
2912
  }
2913
+ if (normalized.imports?.length) {
2914
+ const originalReads = new Map(readPolicies);
2915
+ const protect = (pred, table, path) => {
2916
+ switch (pred.kind) {
2917
+ case "and":
2918
+ case "or":
2919
+ return { ...pred, operands: pred.operands.map((p) => protect(p, table, path)) };
2920
+ case "not":
2921
+ return { ...pred, operand: protect(pred.operand, table, path) };
2922
+ case "exists":
2923
+ case "aggPredicate": {
2924
+ const target = tables[table]?.links.find((link) => link.name === pred.link)?.target;
2925
+ if (!target) return pred;
2926
+ let where = pred.where ? protect(pred.where, target, path) : CONST_TRUE;
2927
+ if (tables[table]?.namespace !== tables[target]?.namespace) {
2928
+ if (path.includes(target)) {
2929
+ diagnostics.push({
2930
+ severity: "error",
2931
+ code: "cyclic-cross-namespace-policy",
2932
+ table,
2933
+ message: `cross-namespace read policies form a cycle through '${target}'`
2934
+ });
2935
+ return CONST_FALSE;
2936
+ }
2937
+ where = andPred(where, protect(originalReads.get(target) ?? CONST_TRUE, target, [...path, target]));
2938
+ }
2939
+ return { ...pred, where };
2940
+ }
2941
+ default:
2942
+ return pred;
2943
+ }
2944
+ };
2945
+ for (const policies of [readPolicies, writePolicies, insertPolicies, updatePolicies]) {
2946
+ for (const [table, policy] of policies) policies.set(table, protect(policy, table, [table]));
2947
+ }
2948
+ }
2004
2949
  const tableByTypename = new Map(Object.entries(tables).map(([key, meta]) => [meta.typename, key]));
2005
2950
  const queryRoots = /* @__PURE__ */ new Map();
2006
2951
  const mutationRoots = /* @__PURE__ */ new Map();
2007
2952
  for (const table of relations) {
2953
+ if (table.internal === true) continue;
2008
2954
  const roots = queryRootNames(table.table);
2009
2955
  queryRoots.set(roots.select, { kind: "select", table: table.table });
2010
2956
  if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
@@ -2020,6 +2966,7 @@ function buildCompileContext(rawCanonical) {
2020
2966
  mutationRoots.set(field, { op, table: table.table });
2021
2967
  }
2022
2968
  }
2969
+ if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
2023
2970
  }
2024
2971
  for (const table of relations) {
2025
2972
  for (const mutation of table.cowMutations ?? []) {
@@ -2075,6 +3022,7 @@ function buildCompileContext(rawCanonical) {
2075
3022
  return {
2076
3023
  ctx: {
2077
3024
  namespace: canonical.namespace,
3025
+ composed: !!normalized.imports?.length,
2078
3026
  tables,
2079
3027
  tableByTypename,
2080
3028
  queryRoots,
@@ -2090,6 +3038,49 @@ function buildCompileContext(rawCanonical) {
2090
3038
  diagnostics
2091
3039
  };
2092
3040
  }
3041
+ function registerResolvedRoots({ table, mutationRoots }) {
3042
+ const names = tableNames(table.table);
3043
+ const canInsert = table.policies.write !== void 0 || table.policies.insert !== void 0;
3044
+ const canUpdate = table.policies.write !== void 0 || table.policies.update !== void 0;
3045
+ const add = (field, action) => mutationRoots.set(field, { op: "resolvedWrite", action, table: table.table });
3046
+ if (canInsert) add(names.insertOneRoot, "insert");
3047
+ if (canUpdate) {
3048
+ add(names.updateByPkRoot, "update");
3049
+ add(names.revertByPkRoot, "revert");
3050
+ if ((table.resolved?.deletedBy.length ?? 0) > 0) add(names.deleteByPkRoot, "delete");
3051
+ }
3052
+ }
3053
+ function cellMap(entries) {
3054
+ const cells = {};
3055
+ for (const entry of entries) {
3056
+ if (entry.cell) cells[entry.cell.of] = { ...cells[entry.cell.of], [entry.cell.part]: entry.name };
3057
+ }
3058
+ return Object.keys(cells).length > 0 ? cells : void 0;
3059
+ }
3060
+ function resolvedWritable(entries) {
3061
+ const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
3062
+ return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
3063
+ }
3064
+ function resolvedMeta({ resolved, pk }) {
3065
+ const manual = resolved.sources.find((source) => source.kind === "manual");
3066
+ return {
3067
+ pk,
3068
+ manual: manual?.name ?? "",
3069
+ deletedBy: [...resolved.deletedBy],
3070
+ ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
3071
+ sources: resolved.sources.map((source) => ({
3072
+ name: source.name,
3073
+ kind: source.kind,
3074
+ table: source.table,
3075
+ columns: [...source.columns],
3076
+ ...source.key !== void 0 ? { key: source.key } : {},
3077
+ ...source.fetch !== void 0 ? { fetch: source.fetch } : {},
3078
+ ...source.function !== void 0 ? { function: source.function } : {},
3079
+ ...source.prefetchDefaults !== void 0 ? { prefetchDefaults: source.prefetchDefaults } : {},
3080
+ ...source.writeback !== void 0 ? { writeback: source.writeback } : {}
3081
+ }))
3082
+ };
3083
+ }
2093
3084
  function flattenSelectionSet(selectionSet, fragments, stack) {
2094
3085
  const selections = [];
2095
3086
  for (const selection of selectionSet.selections) {
@@ -2100,18 +3091,18 @@ function flattenSelectionSet(selectionSet, fragments, stack) {
2100
3091
  continue;
2101
3092
  }
2102
3093
  if (selection.kind === Kind.FRAGMENT_SPREAD) {
2103
- const name = selection.name.value;
3094
+ const name2 = selection.name.value;
2104
3095
  if (selection.directives && selection.directives.length > 0) {
2105
- throw new DocumentCompileError(`directives on fragment spread '...${name}' are not supported`, "fragment-directive");
3096
+ throw new DocumentCompileError(`directives on fragment spread '...${name2}' are not supported`, "fragment-directive");
2106
3097
  }
2107
- if (stack.includes(name)) {
2108
- throw new DocumentCompileError(`fragment cycle detected through '${name}'`, "fragment-cycle");
3098
+ if (stack.includes(name2)) {
3099
+ throw new DocumentCompileError(`fragment cycle detected through '${name2}'`, "fragment-cycle");
2109
3100
  }
2110
- const fragment = fragments.get(name);
3101
+ const fragment = fragments.get(name2);
2111
3102
  if (!fragment) {
2112
- throw new DocumentCompileError(`unknown fragment '${name}'`, "unknown-fragment");
3103
+ throw new DocumentCompileError(`unknown fragment '${name2}'`, "unknown-fragment");
2113
3104
  }
2114
- const inlined2 = flattenSelectionSet(fragment.selectionSet, fragments, [...stack, name]);
3105
+ const inlined2 = flattenSelectionSet(fragment.selectionSet, fragments, [...stack, name2]);
2115
3106
  selections.push(...inlined2.selections);
2116
3107
  continue;
2117
3108
  }
@@ -2147,6 +3138,136 @@ function computeOpsHash(opHashes) {
2147
3138
  const digest = createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
2148
3139
  return `sha256:${digest}`;
2149
3140
  }
3141
+ var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "localOnly"]);
3142
+ var fail = (message, code) => {
3143
+ throw new DocumentCompileError(message, code);
3144
+ };
3145
+ function readArgs(node, name2) {
3146
+ return (node.directives ?? []).filter((directive) => directive.name.value === name2).map((directive) => {
3147
+ const args = {};
3148
+ for (const arg of directive.arguments ?? []) {
3149
+ const key = arg.name.value;
3150
+ if (key === "source" || key === "maxStaleness") {
3151
+ if (arg.value.kind !== Kind.STRING) fail(`@${name2}(${key}:) must be a string literal`, "invalid-source-directive");
3152
+ args[key] = arg.value.value;
3153
+ } else if (key === "mode" && name2 === "writeback") {
3154
+ if (arg.value.kind !== Kind.ENUM || !["SYNC", "ASYNC"].includes(arg.value.value)) {
3155
+ fail("@writeback(mode:) must be SYNC or ASYNC", "invalid-source-directive");
3156
+ }
3157
+ args.mode = arg.value.value === "SYNC" ? "sync" : "async";
3158
+ } else {
3159
+ fail(`@${name2} does not take argument '${key}'`, "invalid-source-directive");
3160
+ }
3161
+ }
3162
+ if (!args.source) fail(`@${name2} needs a source: argument`, "invalid-source-directive");
3163
+ return args;
3164
+ });
3165
+ }
3166
+ function prefetchSourceFor({ resolved, name: name2, directive, table }) {
3167
+ if (!resolved) return fail(`@${directive} is only valid on resolved tables \u2014 '${table}' is not one`, "invalid-source-directive");
3168
+ const source = resolved.sources.find((entry) => entry.name === name2);
3169
+ if (!source || source.kind !== "prefetch") {
3170
+ const options = resolved.sources.filter((entry) => entry.kind === "prefetch").map((entry) => entry.name);
3171
+ return fail(`@${directive}(source: "${name2}") on '${table}' must name a prefetch source (${options.join(", ") || "none declared"})`, "invalid-source-directive");
3172
+ }
3173
+ return source;
3174
+ }
3175
+ function applyPrefetchDirectives({ node, root, meta }) {
3176
+ const directives = readArgs(node, "prefetch");
3177
+ for (const name2 of ["writeback", "localOnly"]) {
3178
+ if (readArgs(node, name2).length > 0) fail(`@${name2} is only valid on resolved-table mutation roots`, "invalid-source-directive");
3179
+ }
3180
+ if (directives.length === 0) return root;
3181
+ if (root.kind !== "select" && root.kind !== "byPk") {
3182
+ return fail(`@prefetch is only valid on select and byPk roots, not '${root.alias}'`, "invalid-source-directive");
3183
+ }
3184
+ const seen = /* @__PURE__ */ new Set();
3185
+ const plans = directives.map((directive) => {
3186
+ const source = prefetchSourceFor({ resolved: meta.resolved, name: directive.source, directive: "prefetch", table: root.table });
3187
+ if (seen.has(source.name)) fail(`@prefetch(source: "${source.name}") appears twice on '${root.alias}'`, "invalid-source-directive");
3188
+ seen.add(source.name);
3189
+ const maxStalenessMs = directive.maxStaleness === void 0 ? void 0 : parseDurationMs(directive.maxStaleness);
3190
+ if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
3191
+ fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
3192
+ }
3193
+ const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk: meta.resolved.pk, source, alias: root.alias });
3194
+ return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
3195
+ });
3196
+ return { ...root, prefetch: plans };
3197
+ }
3198
+ function selectKey({ where, pk, source, alias }) {
3199
+ const leaves = where.kind === "and" ? where.operands : [where];
3200
+ const pinned = leaves.find((leaf) => leaf.kind === "cmp" && leaf.column === pk && (leaf.op === "eq" || leaf.op === "in"));
3201
+ const shape = source.fetch?.shape ?? "single";
3202
+ if (!pinned || pinned.kind !== "cmp") {
3203
+ return fail(
3204
+ `@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)`,
3205
+ "prefetch-shape-mismatch"
3206
+ );
3207
+ }
3208
+ if (pinned.op === "in" && shape !== "batch") {
3209
+ 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");
3210
+ }
3211
+ return pinned.op === "eq" ? { one: pinned.value } : { many: pinned.value };
3212
+ }
3213
+ function applyWritebackDirectives({ node, root, meta }) {
3214
+ if (readArgs(node, "prefetch").length > 0) fail("@prefetch is only valid on query roots", "invalid-source-directive");
3215
+ const writebacks = readArgs(node, "writeback");
3216
+ const localOnly = readArgs(node, "localOnly");
3217
+ if (root.kind !== "mutation" || root.op !== "resolvedWrite" || !meta.resolved) {
3218
+ if (writebacks.length + localOnly.length > 0) fail("@writeback/@localOnly are only valid on resolved-table mutation roots", "invalid-source-directive");
3219
+ return root;
3220
+ }
3221
+ const resolved = meta.resolved;
3222
+ const declared = /* @__PURE__ */ new Map();
3223
+ const bindings = [];
3224
+ for (const directive of writebacks) {
3225
+ const source = prefetchSourceFor({ resolved, name: directive.source, directive: "writeback", table: root.table });
3226
+ if (!source.writeback) fail(`@writeback(source: "${source.name}"): source '${source.name}' declares no writeback columns`, "invalid-source-directive");
3227
+ if (root.resolvedAction !== "update") {
3228
+ fail(`@writeback on '${root.alias}': only update roots write back in v1 (use @localOnly for ${root.resolvedAction ?? "this"} roots)`, "writeback-shape-mismatch");
3229
+ }
3230
+ if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
3231
+ declared.set(source.name, "writeback");
3232
+ bindings.push({ source: source.name, mode: directive.mode ?? source.writeback.defaultMode });
3233
+ }
3234
+ for (const directive of localOnly) {
3235
+ const source = prefetchSourceFor({ resolved, name: directive.source, directive: "localOnly", table: root.table });
3236
+ if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
3237
+ declared.set(source.name, "localOnly");
3238
+ }
3239
+ assertWritebacksDeclared({ root, meta, resolved, declared });
3240
+ return {
3241
+ ...root,
3242
+ ...bindings.length > 0 ? { writeback: bindings } : {},
3243
+ ...localOnly.length > 0 ? { localOnly: localOnly.map((directive) => directive.source) } : {}
3244
+ };
3245
+ }
3246
+ function assertWritebacksDeclared({ root, meta, resolved, declared }) {
3247
+ const written = writtenColumns({ root, meta });
3248
+ for (const source of resolved.sources) {
3249
+ const columns = source.writeback?.columns.filter((column) => written.has(column)) ?? [];
3250
+ if (columns.length > 0 && !declared.has(source.name)) {
3251
+ fail(
3252
+ `'${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`,
3253
+ "writeback-undeclared"
3254
+ );
3255
+ }
3256
+ }
3257
+ }
3258
+ function writtenColumns({ root, meta }) {
3259
+ if (root.resolvedAction === "update") {
3260
+ if (!root.set || "slot" in root.set) return new Set(meta.settable);
3261
+ return new Set(root.set.entries.map((entry) => entry.column));
3262
+ }
3263
+ if (root.resolvedAction === "insert") {
3264
+ if (!root.objects || "slot" in root.objects) return new Set(meta.insertable);
3265
+ return new Set(root.objects.rows.flatMap((row) => row.map((entry) => entry.column)));
3266
+ }
3267
+ return /* @__PURE__ */ new Set();
3268
+ }
3269
+
3270
+ // src/compiler-gql/lower.ts
2150
3271
  var DEFAULT_LIMIT = 100;
2151
3272
  var MAX_LIMIT = 1e3;
2152
3273
  var MAX_GROUPS = 1e3;
@@ -2184,7 +3305,7 @@ var ORDER_DIRECTIONS = {
2184
3305
  DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
2185
3306
  };
2186
3307
  var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
2187
- function fail(message, code, candidates = []) {
3308
+ function fail2(message, code, candidates = []) {
2188
3309
  throw new DocumentCompileError(message, code, candidates);
2189
3310
  }
2190
3311
  function argsByName(field) {
@@ -2192,16 +3313,16 @@ function argsByName(field) {
2192
3313
  }
2193
3314
  function* aggregateBlockFields(selectionSet) {
2194
3315
  for (const node of selectionSet.selections) {
2195
- if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3316
+ if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
2196
3317
  if (node.name.value === "__typename") continue;
2197
- if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
3318
+ if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
2198
3319
  yield node;
2199
3320
  }
2200
3321
  }
2201
3322
  function assertKnownArgs(field, allowed) {
2202
3323
  for (const arg of field.arguments ?? []) {
2203
3324
  if (!allowed.includes(arg.name.value)) {
2204
- fail(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
3325
+ fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
2205
3326
  }
2206
3327
  }
2207
3328
  }
@@ -2214,27 +3335,31 @@ var OperationLowerer = class {
2214
3335
  meta(table) {
2215
3336
  return this.ctx.tables[table];
2216
3337
  }
2217
- column(table, name) {
2218
- return this.meta(table).columns.find((c) => c.name === name);
3338
+ /** A data column — cell-metadata columns are reachable only through `cells { … }`. */
3339
+ column(table, name2) {
3340
+ const meta = this.meta(table);
3341
+ const column = meta.columns.find((c) => c.name === name2);
3342
+ return column && !isCellColumn(meta, name2) ? column : void 0;
2219
3343
  }
2220
- link(table, name) {
2221
- return this.meta(table).links.find((l) => l.name === name);
3344
+ link(table, name2) {
3345
+ return this.meta(table).links.find((l) => l.name === name2);
2222
3346
  }
2223
3347
  fieldCandidates(table) {
2224
3348
  const meta = this.meta(table);
2225
3349
  return [
2226
- ...meta.columns.map((c) => c.name),
3350
+ ...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
3351
+ ...meta.cells ? [CELLS_FIELD] : [],
2227
3352
  ...meta.links.map((l) => l.name),
2228
3353
  ...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
2229
3354
  ];
2230
3355
  }
2231
3356
  /** Validate that a whole-variable argument is the declared §4f slot for this table. */
2232
- slot(name, input, table) {
2233
- const variable = this.variables.get(name);
3357
+ slot(name2, input, table) {
3358
+ const variable = this.variables.get(name2);
2234
3359
  if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
2235
- fail(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
3360
+ fail2(`variable $${name2} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
2236
3361
  }
2237
- return name;
3362
+ return name2;
2238
3363
  }
2239
3364
  // -------------------------------------------------------------------------
2240
3365
  // Values
@@ -2257,14 +3382,14 @@ var OperationLowerer = class {
2257
3382
  const items = node.values.map((item) => {
2258
3383
  const lowered = this.lowerValue(item);
2259
3384
  if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
2260
- fail("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
3385
+ fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
2261
3386
  }
2262
3387
  return lowered.value;
2263
3388
  });
2264
3389
  return { kind: "literal", value: items };
2265
3390
  }
2266
3391
  case Kind.OBJECT:
2267
- fail("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
3392
+ fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
2268
3393
  }
2269
3394
  }
2270
3395
  // -------------------------------------------------------------------------
@@ -2274,7 +3399,7 @@ var OperationLowerer = class {
2274
3399
  if (node.kind === Kind.VARIABLE) {
2275
3400
  return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
2276
3401
  }
2277
- if (node.kind !== Kind.OBJECT) fail(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
3402
+ if (node.kind !== Kind.OBJECT) fail2(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
2278
3403
  const operands = [];
2279
3404
  for (const field of node.fields) {
2280
3405
  const key = field.name.value;
@@ -2305,45 +3430,45 @@ var OperationLowerer = class {
2305
3430
  continue;
2306
3431
  }
2307
3432
  }
2308
- fail(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
3433
+ fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
2309
3434
  }
2310
3435
  if (operands.length === 0) return CONST_TRUE;
2311
3436
  return operands.length === 1 ? operands[0] : { kind: "and", operands };
2312
3437
  }
2313
3438
  lowerComparison(node, column, table) {
2314
3439
  if (node.kind !== Kind.OBJECT) {
2315
- fail(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
3440
+ fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
2316
3441
  }
2317
3442
  return node.fields.map((field) => {
2318
3443
  const op = BOOLEXP_OPS[field.name.value];
2319
3444
  if (!op) {
2320
- fail(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
3445
+ fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
2321
3446
  }
2322
3447
  return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
2323
3448
  });
2324
3449
  }
2325
3450
  /** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
2326
3451
  lowerAggPredicate(node, link, target) {
2327
- if (node.kind !== Kind.OBJECT) fail(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
3452
+ if (node.kind !== Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
2328
3453
  const countField = node.fields.find((f) => f.name.value === "count");
2329
3454
  if (!countField || node.fields.length !== 1) {
2330
- fail(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
3455
+ fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
2331
3456
  }
2332
- if (countField.value.kind !== Kind.OBJECT) fail(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
3457
+ if (countField.value.kind !== Kind.OBJECT) fail2(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
2333
3458
  let predicate;
2334
3459
  let where;
2335
3460
  for (const field of countField.value.fields) {
2336
3461
  if (field.name.value === "predicate") {
2337
3462
  if (field.value.kind !== Kind.OBJECT || field.value.fields.length !== 1) {
2338
- fail(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
3463
+ fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
2339
3464
  }
2340
3465
  const opField = field.value.fields[0];
2341
3466
  const opName = opField.name.value;
2342
3467
  if (!AGG_PREDICATE_OPS.includes(opName)) {
2343
- fail(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
3468
+ fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
2344
3469
  }
2345
3470
  if (opField.value.kind !== Kind.INT) {
2346
- fail(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
3471
+ fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
2347
3472
  }
2348
3473
  predicate = { op: opName.slice(1), value: Number(opField.value.value) };
2349
3474
  continue;
@@ -2352,9 +3477,9 @@ var OperationLowerer = class {
2352
3477
  where = this.lowerBoolExp(field.value, target);
2353
3478
  continue;
2354
3479
  }
2355
- fail(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
3480
+ fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
2356
3481
  }
2357
- if (!predicate) fail(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
3482
+ if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
2358
3483
  return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
2359
3484
  }
2360
3485
  lowerOrderBy(node, table) {
@@ -2364,7 +3489,7 @@ var OperationLowerer = class {
2364
3489
  const items = node.kind === Kind.LIST ? node.values : [node];
2365
3490
  const lowered = [];
2366
3491
  for (const item of items) {
2367
- if (item.kind !== Kind.OBJECT) fail(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
3492
+ if (item.kind !== Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
2368
3493
  for (const field of item.fields) {
2369
3494
  const key = field.name.value;
2370
3495
  const column = this.column(table, key);
@@ -2375,25 +3500,25 @@ var OperationLowerer = class {
2375
3500
  const link = this.link(table, key);
2376
3501
  if (link && link.kind !== "toMany") {
2377
3502
  if (field.value.kind !== Kind.OBJECT || field.value.fields.length !== 1) {
2378
- fail(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
3503
+ fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
2379
3504
  }
2380
3505
  const inner = field.value.fields[0];
2381
3506
  const targetColumn = this.column(link.target, inner.name.value);
2382
3507
  if (!targetColumn) {
2383
- fail(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
3508
+ fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
2384
3509
  }
2385
3510
  lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
2386
3511
  continue;
2387
3512
  }
2388
- fail(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
3513
+ fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
2389
3514
  }
2390
3515
  }
2391
3516
  return lowered;
2392
3517
  }
2393
3518
  lowerDirection(node, table, column) {
2394
- if (node.kind !== Kind.ENUM) fail(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
3519
+ if (node.kind !== Kind.ENUM) fail2(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
2395
3520
  const direction = ORDER_DIRECTIONS[node.value];
2396
- if (!direction) fail(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
3521
+ if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
2397
3522
  return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
2398
3523
  }
2399
3524
  lowerDistinctOn(node, table) {
@@ -2402,29 +3527,29 @@ var OperationLowerer = class {
2402
3527
  }
2403
3528
  const items = node.kind === Kind.LIST ? node.values : [node];
2404
3529
  return items.map((item) => {
2405
- if (item.kind !== Kind.ENUM) fail(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
3530
+ if (item.kind !== Kind.ENUM) fail2(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
2406
3531
  if (!this.column(table, item.value)) {
2407
- fail(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3532
+ fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
2408
3533
  }
2409
3534
  return item.value;
2410
3535
  });
2411
3536
  }
2412
3537
  lowerLimit(node) {
2413
- const base = { default: DEFAULT_LIMIT, max: MAX_LIMIT };
2414
- if (!node) return base;
2415
- if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
3538
+ const base2 = { default: DEFAULT_LIMIT, max: MAX_LIMIT };
3539
+ if (!node) return base2;
3540
+ if (node.kind === Kind.VARIABLE) return { ...base2, variable: node.name.value };
2416
3541
  if (node.kind === Kind.INT) {
2417
3542
  const value = Number(node.value);
2418
- if (value < 1) fail("limit must be a positive integer", "invalid-limit");
2419
- return { ...base, value: Math.min(value, MAX_LIMIT) };
3543
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
3544
+ return { ...base2, value: Math.min(value, MAX_LIMIT) };
2420
3545
  }
2421
- fail("limit must be an integer literal or a variable", "invalid-limit");
3546
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
2422
3547
  }
2423
3548
  lowerOffset(node) {
2424
3549
  if (!node) return void 0;
2425
3550
  if (node.kind === Kind.VARIABLE) return { variable: node.name.value };
2426
3551
  if (node.kind === Kind.INT) return { value: Number(node.value) };
2427
- fail("offset must be an integer literal or a variable", "invalid-offset");
3552
+ fail2("offset must be an integer literal or a variable", "invalid-offset");
2428
3553
  }
2429
3554
  // -------------------------------------------------------------------------
2430
3555
  // Selections
@@ -2433,27 +3558,32 @@ var OperationLowerer = class {
2433
3558
  const meta = this.meta(table);
2434
3559
  const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
2435
3560
  for (const node of selectionSet.selections) {
2436
- if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3561
+ if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
2437
3562
  if (node.directives && node.directives.length > 0) {
2438
- fail(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
3563
+ fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
2439
3564
  }
2440
- const name = node.name.value;
2441
- if (name === "__typename") continue;
2442
- const alias = node.alias?.value ?? name;
2443
- const column = this.column(table, name);
3565
+ const name2 = node.name.value;
3566
+ if (name2 === "__typename") continue;
3567
+ const alias = node.alias?.value ?? name2;
3568
+ const column = this.column(table, name2);
2444
3569
  if (column) {
2445
3570
  if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
2446
- fail(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
3571
+ fail2(`column '${table}.${name2}' takes no arguments or sub-selection`, "invalid-selection");
2447
3572
  }
2448
- selection.columns.push({ alias, column: name });
3573
+ selection.columns.push({ alias, column: name2 });
3574
+ continue;
3575
+ }
3576
+ if (name2 === CELLS_FIELD && meta.cells) {
3577
+ if (selection.cells) fail2(`'${CELLS_FIELD}' may be selected once per object`, "invalid-selection");
3578
+ selection.cells = this.lowerCells(node, table, alias);
2449
3579
  continue;
2450
3580
  }
2451
- const link = this.link(table, name);
3581
+ const link = this.link(table, name2);
2452
3582
  if (link) {
2453
- if (!node.selectionSet) fail(`link '${table}.${name}' needs a selection set`, "invalid-selection");
3583
+ if (!node.selectionSet) fail2(`link '${table}.${name2}' needs a selection set`, "invalid-selection");
2454
3584
  const relation = {
2455
3585
  alias,
2456
- link: name,
3586
+ link: name2,
2457
3587
  selection: this.lowerSelection(node.selectionSet, link.target)
2458
3588
  };
2459
3589
  if (link.kind === "toMany") {
@@ -2469,15 +3599,15 @@ var OperationLowerer = class {
2469
3599
  if (offset) relation.offset = offset;
2470
3600
  }
2471
3601
  } else if (node.arguments && node.arguments.length > 0) {
2472
- fail(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
3602
+ fail2(`to-one link '${table}.${name2}' takes no arguments`, "invalid-selection");
2473
3603
  }
2474
3604
  selection.relations.push(relation);
2475
3605
  continue;
2476
3606
  }
2477
- if (name.endsWith("Aggregate")) {
2478
- const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
3607
+ if (name2.endsWith("Aggregate")) {
3608
+ const aggLink = this.link(table, name2.slice(0, -"Aggregate".length));
2479
3609
  if (aggLink && aggLink.kind === "toMany") {
2480
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3610
+ if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
2481
3611
  assertKnownArgs(node, ["where"]);
2482
3612
  const whereArg = argsByName(node).get("where");
2483
3613
  const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
@@ -2490,28 +3620,75 @@ var OperationLowerer = class {
2490
3620
  continue;
2491
3621
  }
2492
3622
  }
2493
- fail(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
3623
+ fail2(`unknown field '${name2}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
2494
3624
  }
2495
3625
  return selection;
2496
3626
  }
3627
+ /**
3628
+ * `cells { <column> { at by … } }` — typed per-cell metadata. Plain tables
3629
+ * offer `at`/`by`; resolved tables add the winning `source` and newer
3630
+ * `conflicts { source at value }`.
3631
+ */
3632
+ lowerCells(node, table, alias) {
3633
+ const meta = this.meta(table);
3634
+ const cells = meta.cells ?? {};
3635
+ if (node.arguments && node.arguments.length > 0) fail2(`'${CELLS_FIELD}' takes no arguments`, "invalid-selection");
3636
+ if (!node.selectionSet) fail2(`'${CELLS_FIELD}' needs a selection set`, "invalid-selection");
3637
+ const fields = [];
3638
+ for (const cellNode of fieldNodes(node.selectionSet)) {
3639
+ const column = cellNode.name.value;
3640
+ const parts = cells[column];
3641
+ if (!parts || parts.at === void 0 && parts.source === void 0) {
3642
+ fail2(`unknown cell '${column}' on '${table}'`, "unknown-field", Object.keys(cells));
3643
+ }
3644
+ if (!cellNode.selectionSet) fail2(`cell '${column}' needs a selection set`, "invalid-selection");
3645
+ const resolved = parts.source !== void 0;
3646
+ const allowed = resolved ? ["source", "at", "by", "conflicts"] : ["at", "by"];
3647
+ const selected = [];
3648
+ for (const partNode of fieldNodes(cellNode.selectionSet)) {
3649
+ const part = partNode.name.value;
3650
+ if (!allowed.includes(part)) fail2(`unknown field '${part}' on cell '${column}'`, "unknown-field", allowed);
3651
+ const partAlias = partNode.alias?.value ?? part;
3652
+ if (part !== "conflicts") {
3653
+ if (partNode.selectionSet) fail2(`cell field '${part}' takes no sub-selection`, "invalid-selection");
3654
+ selected.push({ alias: partAlias, part });
3655
+ continue;
3656
+ }
3657
+ if (!partNode.selectionSet) fail2(`'conflicts' needs a selection set`, "invalid-selection");
3658
+ const conflictFields = fieldNodes(partNode.selectionSet).map((fieldNode) => {
3659
+ const field = fieldNode.name.value;
3660
+ if (field !== "source" && field !== "at" && field !== "value") fail2(`unknown field '${field}' on a cell conflict`, "unknown-field", ["source", "at", "value"]);
3661
+ return { alias: fieldNode.alias?.value ?? field, field };
3662
+ });
3663
+ selected.push({ alias: partAlias, part: "conflicts", fields: conflictFields, typename: CELL_TYPENAMES.conflict });
3664
+ }
3665
+ fields.push({
3666
+ alias: cellNode.alias?.value ?? column,
3667
+ column,
3668
+ typename: resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp,
3669
+ parts: selected
3670
+ });
3671
+ }
3672
+ return { alias, typename: cellsTypename(meta.typename), fields };
3673
+ }
2497
3674
  /** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
2498
3675
  lowerAggregateSelection(selectionSet, table) {
2499
3676
  const result = {};
2500
3677
  let sawAggregate = false;
2501
3678
  for (const node of aggregateBlockFields(selectionSet)) {
2502
- const name = node.name.value;
2503
- if (name === "nodes") {
2504
- if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
3679
+ const name2 = node.name.value;
3680
+ if (name2 === "nodes") {
3681
+ if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
2505
3682
  result.nodes = this.lowerSelection(node.selectionSet, table);
2506
3683
  continue;
2507
3684
  }
2508
- if (name !== "aggregate") fail(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
2509
- if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3685
+ if (name2 !== "aggregate") fail2(`unknown aggregate field '${name2}'`, "invalid-aggregate", ["aggregate", "nodes"]);
3686
+ if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
2510
3687
  sawAggregate = true;
2511
- if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
3688
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
2512
3689
  Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
2513
3690
  }
2514
- if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
3691
+ if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
2515
3692
  return result;
2516
3693
  }
2517
3694
  /**
@@ -2526,26 +3703,26 @@ var OperationLowerer = class {
2526
3703
  let sawAggregate = false;
2527
3704
  let sawKey = false;
2528
3705
  for (const node of aggregateBlockFields(selectionSet)) {
2529
- const name = node.name.value;
2530
- if (name === "key") {
2531
- if (sawKey) fail("one key block per grouped aggregate", "invalid-aggregate");
3706
+ const name2 = node.name.value;
3707
+ if (name2 === "key") {
3708
+ if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
2532
3709
  sawKey = true;
2533
- if (!node.selectionSet) fail("'key' needs a selection set", "invalid-aggregate");
3710
+ if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
2534
3711
  result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
2535
3712
  continue;
2536
3713
  }
2537
- if (name === "nodes") {
2538
- fail("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
3714
+ if (name2 === "nodes") {
3715
+ fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
2539
3716
  }
2540
- if (name !== "aggregate") fail(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
2541
- if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3717
+ if (name2 !== "aggregate") fail2(`unknown grouped-aggregate field '${name2}'`, "invalid-aggregate", ["key", "aggregate"]);
3718
+ if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
2542
3719
  sawAggregate = true;
2543
- if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
3720
+ if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
2544
3721
  Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
2545
3722
  }
2546
3723
  const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
2547
3724
  if (result.key.length === 0 && !rendersAggregate) {
2548
- fail(
3725
+ fail2(
2549
3726
  "grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
2550
3727
  "invalid-aggregate",
2551
3728
  ["key", "aggregate"]
@@ -2562,20 +3739,20 @@ var OperationLowerer = class {
2562
3739
  lowerGroupKeySelection(selectionSet, table, groupColumns) {
2563
3740
  const key = [];
2564
3741
  for (const node of selectionSet.selections) {
2565
- if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
2566
- const name = node.name.value;
2567
- if (name === "__typename") continue;
2568
- if (!this.column(table, name)) {
2569
- fail(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
2570
- }
2571
- if (!groupColumns.has(name)) {
2572
- fail(
2573
- `key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
3742
+ if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
3743
+ const name2 = node.name.value;
3744
+ if (name2 === "__typename") continue;
3745
+ if (!this.column(table, name2)) {
3746
+ fail2(`unknown key column '${name2}' on '${table}'`, "unknown-column", [...groupColumns]);
3747
+ }
3748
+ if (!groupColumns.has(name2)) {
3749
+ fail2(
3750
+ `key column '${name2}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
2574
3751
  "invalid-group-key",
2575
3752
  [...groupColumns]
2576
3753
  );
2577
3754
  }
2578
- key.push({ alias: node.alias?.value ?? name, column: name });
3755
+ key.push({ alias: node.alias?.value ?? name2, column: name2 });
2579
3756
  }
2580
3757
  return key;
2581
3758
  }
@@ -2592,21 +3769,21 @@ var OperationLowerer = class {
2592
3769
  if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
2593
3770
  const distinct = args.get("distinct");
2594
3771
  if (distinct) {
2595
- if (distinct.value.kind !== Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
3772
+ if (distinct.value.kind !== Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
2596
3773
  count.distinct = distinct.value.value;
2597
3774
  }
2598
3775
  result.count = count;
2599
3776
  continue;
2600
3777
  }
2601
3778
  if (aggName === "sum" || aggName === "avg") {
2602
- if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
3779
+ if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
2603
3780
  const columns = [];
2604
3781
  for (const columnField of aggField.selectionSet.selections) {
2605
- if (columnField.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3782
+ if (columnField.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
2606
3783
  if (columnField.name.value === "__typename") continue;
2607
3784
  const column = this.column(table, columnField.name.value);
2608
3785
  if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
2609
- fail(
3786
+ fail2(
2610
3787
  `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
2611
3788
  "invalid-aggregate",
2612
3789
  this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
@@ -2617,7 +3794,7 @@ var OperationLowerer = class {
2617
3794
  result[aggName] = columns;
2618
3795
  continue;
2619
3796
  }
2620
- fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
3797
+ fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
2621
3798
  }
2622
3799
  return result;
2623
3800
  }
@@ -2627,41 +3804,43 @@ var OperationLowerer = class {
2627
3804
  lowerRoots(operation, kind) {
2628
3805
  const roots = [];
2629
3806
  for (const node of operation.selectionSet.selections) {
2630
- if (node.kind !== Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
3807
+ if (node.kind !== Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
2631
3808
  if (node.selectionSet) visit(node.selectionSet, {
2632
3809
  Directive(directive) {
2633
3810
  if (directive.name.value === "assertAffectedRows") {
2634
- fail("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
3811
+ fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
2635
3812
  }
2636
3813
  }
2637
3814
  });
2638
3815
  const assertion = this.parseAffectedRowAssertion(node, kind);
2639
3816
  const gate = this.parseRootDirectives(node);
2640
3817
  if (gate.omit) continue;
2641
- const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
3818
+ const lowered = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
3819
+ const meta = this.meta(lowered.table);
3820
+ const root = kind === "query" ? applyPrefetchDirectives({ node, root: lowered, meta }) : applyWritebackDirectives({ node, root: lowered, meta });
2642
3821
  const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
2643
3822
  roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
2644
3823
  }
2645
- if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
3824
+ if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
2646
3825
  return roots;
2647
3826
  }
2648
3827
  parseAffectedRowAssertion(node, kind) {
2649
3828
  const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
2650
3829
  if (!assertions.length) return void 0;
2651
3830
  if (kind !== "mutation" || assertions.length !== 1) {
2652
- fail("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
3831
+ fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
2653
3832
  }
2654
3833
  if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
2655
- fail("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
3834
+ fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
2656
3835
  }
2657
3836
  const args = assertions[0].arguments;
2658
3837
  const arg = args?.[0];
2659
3838
  if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== Kind.INT) {
2660
- fail("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
3839
+ fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
2661
3840
  }
2662
3841
  const count = Number(arg.value.value);
2663
3842
  if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
2664
- fail("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
3843
+ fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
2665
3844
  }
2666
3845
  return count;
2667
3846
  }
@@ -2669,34 +3848,35 @@ var OperationLowerer = class {
2669
3848
  let includeIf;
2670
3849
  let omit = false;
2671
3850
  for (const directive of node.directives ?? []) {
2672
- const name = directive.name.value;
2673
- if (name === "assertAffectedRows") continue;
2674
- if (name !== "include" && name !== "skip") {
2675
- fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
3851
+ const name2 = directive.name.value;
3852
+ if (name2 === "assertAffectedRows") continue;
3853
+ if (SOURCE_DIRECTIVES.has(name2)) continue;
3854
+ if (name2 !== "include" && name2 !== "skip") {
3855
+ fail2(`directive @${name2} is not supported on fields`, "unknown-directive", ["include", "skip"]);
2676
3856
  }
2677
3857
  const ifArg = directive.arguments?.find((a) => a.name.value === "if");
2678
- if (!ifArg) fail(`@${name} needs an if: argument`, "invalid-directive");
3858
+ if (!ifArg) fail2(`@${name2} needs an if: argument`, "invalid-directive");
2679
3859
  if (ifArg.value.kind === Kind.BOOLEAN) {
2680
- const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
3860
+ const keep = name2 === "include" ? ifArg.value.value : !ifArg.value.value;
2681
3861
  if (!keep) omit = true;
2682
3862
  continue;
2683
3863
  }
2684
- if (ifArg.value.kind !== Kind.VARIABLE) fail(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
2685
- if (includeIf) fail("at most one of @include/@skip per root field", "invalid-directive");
2686
- includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
3864
+ if (ifArg.value.kind !== Kind.VARIABLE) fail2(`@${name2}(if:) must be a boolean literal or variable`, "invalid-directive");
3865
+ if (includeIf) fail2("at most one of @include/@skip per root field", "invalid-directive");
3866
+ includeIf = { variable: ifArg.value.name.value, negated: name2 === "skip" };
2687
3867
  }
2688
3868
  return omit ? { omit: true } : { includeIf, omit: false };
2689
3869
  }
2690
3870
  lowerQueryRoot(node) {
2691
- const name = node.name.value;
2692
- const alias = node.alias?.value ?? name;
2693
- const rootInfo = this.ctx.queryRoots.get(name);
2694
- if (!rootInfo) fail(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
3871
+ const name2 = node.name.value;
3872
+ const alias = node.alias?.value ?? name2;
3873
+ const rootInfo = this.ctx.queryRoots.get(name2);
3874
+ if (!rootInfo) fail2(`unknown query root '${name2}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
2695
3875
  const { table } = rootInfo;
2696
3876
  const args = argsByName(node);
2697
3877
  if (rootInfo.kind === "select") {
2698
3878
  assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
2699
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3879
+ if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
2700
3880
  const where = args.get("where");
2701
3881
  const orderBy = args.get("orderBy");
2702
3882
  const distinctOn = args.get("distinctOn");
@@ -2715,7 +3895,7 @@ var OperationLowerer = class {
2715
3895
  };
2716
3896
  }
2717
3897
  if (rootInfo.kind === "byPk") {
2718
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3898
+ if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
2719
3899
  const pk = this.lowerPkArgs(node, table);
2720
3900
  return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
2721
3901
  }
@@ -2723,7 +3903,7 @@ var OperationLowerer = class {
2723
3903
  return this.lowerAggregateGroupsRoot(node, { alias, table });
2724
3904
  }
2725
3905
  assertKnownArgs(node, ["where"]);
2726
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3906
+ if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
2727
3907
  const whereArg = args.get("where");
2728
3908
  const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
2729
3909
  return {
@@ -2737,10 +3917,10 @@ var OperationLowerer = class {
2737
3917
  /** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
2738
3918
  lowerAggregateGroupsRoot(node, { alias, table }) {
2739
3919
  assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
2740
- if (!node.selectionSet) fail(`'${node.name.value}' needs a selection set`, "invalid-selection");
3920
+ if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
2741
3921
  const args = argsByName(node);
2742
3922
  const groupByArg = args.get("groupBy");
2743
- if (!groupByArg) fail(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
3923
+ if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
2744
3924
  const whereArg = args.get("where");
2745
3925
  const groupBy = this.lowerGroupBy(groupByArg.value, table);
2746
3926
  const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
@@ -2770,55 +3950,55 @@ var OperationLowerer = class {
2770
3950
  */
2771
3951
  lowerGroupBy(node, table) {
2772
3952
  if (node.kind === Kind.VARIABLE) {
2773
- fail(
3953
+ fail2(
2774
3954
  `groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
2775
3955
  "dynamic-group-by"
2776
3956
  );
2777
3957
  }
2778
3958
  const items = node.kind === Kind.LIST ? node.values : [node];
2779
- if (items.length === 0) fail(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
3959
+ if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
2780
3960
  const seen = /* @__PURE__ */ new Set();
2781
3961
  return items.map((item) => {
2782
3962
  if (item.kind !== Kind.OBJECT) {
2783
- fail(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
3963
+ fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
2784
3964
  }
2785
3965
  const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
2786
3966
  for (const key of fields.keys()) {
2787
3967
  if (key !== "column" && key !== "bucket") {
2788
- fail(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
3968
+ fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
2789
3969
  }
2790
3970
  }
2791
3971
  const columnNode = fields.get("column");
2792
- if (!columnNode) fail(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
3972
+ if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
2793
3973
  if (columnNode.kind !== Kind.ENUM) {
2794
- fail(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
3974
+ fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
2795
3975
  }
2796
3976
  const column = this.column(table, columnNode.value);
2797
3977
  if (!column) {
2798
- fail(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3978
+ fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
2799
3979
  }
2800
3980
  if (column.type === "jsonb") {
2801
- fail(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
3981
+ fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
2802
3982
  }
2803
3983
  const bucketNode = fields.get("bucket");
2804
3984
  let bucket;
2805
3985
  if (bucketNode) {
2806
3986
  if (bucketNode.kind !== Kind.ENUM) {
2807
- fail(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3987
+ fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
2808
3988
  }
2809
3989
  bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
2810
3990
  if (!bucket) {
2811
- fail(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3991
+ fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
2812
3992
  }
2813
3993
  if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
2814
- fail(
3994
+ fail2(
2815
3995
  `groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
2816
3996
  "invalid-group-by"
2817
3997
  );
2818
3998
  }
2819
3999
  }
2820
4000
  if (seen.has(columnNode.value)) {
2821
- fail(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
4001
+ fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
2822
4002
  }
2823
4003
  seen.add(columnNode.value);
2824
4004
  return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
@@ -2826,15 +4006,15 @@ var OperationLowerer = class {
2826
4006
  }
2827
4007
  /** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
2828
4008
  lowerGroupLimit(node) {
2829
- const base = { default: MAX_GROUPS, max: MAX_GROUPS };
2830
- if (!node) return base;
2831
- if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
4009
+ const base2 = { default: MAX_GROUPS, max: MAX_GROUPS };
4010
+ if (!node) return base2;
4011
+ if (node.kind === Kind.VARIABLE) return { ...base2, variable: node.name.value };
2832
4012
  if (node.kind === Kind.INT) {
2833
4013
  const value = Number(node.value);
2834
- if (value < 1) fail("limit must be a positive integer", "invalid-limit");
2835
- return { ...base, value: Math.min(value, MAX_GROUPS) };
4014
+ if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
4015
+ return { ...base2, value: Math.min(value, MAX_GROUPS) };
2836
4016
  }
2837
- fail("limit must be an integer literal or a variable", "invalid-limit");
4017
+ fail2("limit must be an integer literal or a variable", "invalid-limit");
2838
4018
  }
2839
4019
  /** byPk-style pk arguments: one argument per primary-key column, exactly. */
2840
4020
  lowerPkArgs(node, table) {
@@ -2843,7 +4023,7 @@ var OperationLowerer = class {
2843
4023
  assertKnownArgs(node, meta.primaryKey);
2844
4024
  return meta.primaryKey.map((column) => {
2845
4025
  const arg = args.get(column);
2846
- if (!arg) fail(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
4026
+ if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
2847
4027
  return { column, value: this.lowerValue(arg.value) };
2848
4028
  });
2849
4029
  }
@@ -2851,25 +4031,28 @@ var OperationLowerer = class {
2851
4031
  // Mutations
2852
4032
  // -------------------------------------------------------------------------
2853
4033
  lowerMutationRoot(node) {
2854
- const name = node.name.value;
2855
- const alias = node.alias?.value ?? name;
2856
- const rootInfo = this.ctx.mutationRoots.get(name);
2857
- if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
4034
+ const name2 = node.name.value;
4035
+ const alias = node.alias?.value ?? name2;
4036
+ const rootInfo = this.ctx.mutationRoots.get(name2);
4037
+ if (!rootInfo) fail2(`unknown mutation root '${name2}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
2858
4038
  if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
2859
4039
  return this.lowerCowRoot(node, rootInfo);
2860
4040
  }
4041
+ if (rootInfo.op === "resolvedWrite") {
4042
+ return this.lowerResolvedRoot(node, rootInfo);
4043
+ }
2861
4044
  const { op, table } = rootInfo;
2862
4045
  const args = argsByName(node);
2863
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
2864
- const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
4046
+ if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
4047
+ const base2 = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
2865
4048
  switch (op) {
2866
4049
  case "insert": {
2867
4050
  assertKnownArgs(node, ["objects", "onConflict"]);
2868
4051
  const objects = args.get("objects");
2869
- if (!objects) fail(`'${name}' needs objects:`, "missing-argument");
4052
+ if (!objects) fail2(`'${name2}' needs objects:`, "missing-argument");
2870
4053
  const response = this.lowerMutationResponse(node.selectionSet, table);
2871
4054
  return {
2872
- ...base,
4055
+ ...base2,
2873
4056
  ...response,
2874
4057
  objects: this.lowerInsertObjects(objects.value, table, { list: true }),
2875
4058
  ...this.lowerOnConflictArg(args.get("onConflict"), table)
@@ -2878,9 +4061,9 @@ var OperationLowerer = class {
2878
4061
  case "insertOne": {
2879
4062
  assertKnownArgs(node, ["object", "onConflict"]);
2880
4063
  const object = args.get("object");
2881
- if (!object) fail(`'${name}' needs object:`, "missing-argument");
4064
+ if (!object) fail2(`'${name2}' needs object:`, "missing-argument");
2882
4065
  return {
2883
- ...base,
4066
+ ...base2,
2884
4067
  objects: this.lowerInsertObjects(object.value, table, { list: false }),
2885
4068
  ...this.lowerOnConflictArg(args.get("onConflict"), table),
2886
4069
  returning: this.lowerSelection(node.selectionSet, table)
@@ -2889,9 +4072,9 @@ var OperationLowerer = class {
2889
4072
  case "update": {
2890
4073
  assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
2891
4074
  const where = args.get("where");
2892
- if (!where) fail(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
4075
+ if (!where) fail2(`'${name2}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
2893
4076
  return {
2894
- ...base,
4077
+ ...base2,
2895
4078
  ...this.lowerMutationResponse(node.selectionSet, table),
2896
4079
  where: this.lowerBoolExp(where.value, table),
2897
4080
  ...this.lowerWriteValues(args, table)
@@ -2900,9 +4083,9 @@ var OperationLowerer = class {
2900
4083
  case "updateByPk": {
2901
4084
  assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
2902
4085
  const pkColumns = args.get("pkColumns");
2903
- if (!pkColumns) fail(`'${name}' needs pkColumns:`, "missing-argument");
4086
+ if (!pkColumns) fail2(`'${name2}' needs pkColumns:`, "missing-argument");
2904
4087
  return {
2905
- ...base,
4088
+ ...base2,
2906
4089
  pk: this.lowerPkColumnsArg(pkColumns.value, table),
2907
4090
  ...this.lowerWriteValues(args, table),
2908
4091
  returning: this.lowerSelection(node.selectionSet, table)
@@ -2911,16 +4094,16 @@ var OperationLowerer = class {
2911
4094
  case "delete": {
2912
4095
  assertKnownArgs(node, ["where"]);
2913
4096
  const where = args.get("where");
2914
- if (!where) fail(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
4097
+ if (!where) fail2(`'${name2}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
2915
4098
  return {
2916
- ...base,
4099
+ ...base2,
2917
4100
  ...this.lowerMutationResponse(node.selectionSet, table),
2918
4101
  where: this.lowerBoolExp(where.value, table)
2919
4102
  };
2920
4103
  }
2921
4104
  case "deleteByPk": {
2922
4105
  return {
2923
- ...base,
4106
+ ...base2,
2924
4107
  pk: this.lowerPkArgs(node, table),
2925
4108
  returning: this.lowerSelection(node.selectionSet, table)
2926
4109
  };
@@ -2935,16 +4118,16 @@ var OperationLowerer = class {
2935
4118
  * the structured fields, never from authored statements.
2936
4119
  */
2937
4120
  lowerCowRoot(node, rootInfo) {
2938
- const name = node.name.value;
2939
- const alias = node.alias?.value ?? name;
4121
+ const name2 = node.name.value;
4122
+ const alias = node.alias?.value ?? name2;
2940
4123
  const { table } = rootInfo;
2941
- if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
4124
+ if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
2942
4125
  const meta = this.meta(table);
2943
4126
  const args = argsByName(node);
2944
4127
  const pkArg = args.get(rootInfo.pk);
2945
- if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
4128
+ if (!pkArg) fail2(`'${name2}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
2946
4129
  const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
2947
- const base = {
4130
+ const base2 = {
2948
4131
  kind: "mutation",
2949
4132
  op: rootInfo.op,
2950
4133
  alias,
@@ -2957,65 +4140,119 @@ var OperationLowerer = class {
2957
4140
  };
2958
4141
  if (rootInfo.op === "cowRevert") {
2959
4142
  assertKnownArgs(node, [rootInfo.pk]);
2960
- return base;
4143
+ return base2;
2961
4144
  }
2962
4145
  assertKnownArgs(node, [rootInfo.pk, ...rootInfo.set]);
2963
4146
  const entries = rootInfo.set.map((column) => {
2964
4147
  const arg = args.get(column);
2965
4148
  if (!arg) {
2966
- fail(
2967
- `'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
4149
+ fail2(
4150
+ `'${name2}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
2968
4151
  "missing-argument",
2969
4152
  [...rootInfo.set]
2970
4153
  );
2971
4154
  }
2972
4155
  if (!meta.settable.includes(column)) {
2973
- fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
4156
+ fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
2974
4157
  }
2975
4158
  return { column, value: this.lowerValue(arg.value) };
2976
4159
  });
2977
- return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
4160
+ return { ...base2, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
4161
+ }
4162
+ /**
4163
+ * Resolved-table roots: insertOne(object), updateByPk(pkColumns, _set),
4164
+ * deleteByPk(pk…), revertByPk(pkColumns, columns). The executor writes only
4165
+ * the touched cells into the manual source; `returning` reads the view.
4166
+ */
4167
+ lowerResolvedRoot(node, rootInfo) {
4168
+ const name2 = node.name.value;
4169
+ const alias = node.alias?.value ?? name2;
4170
+ const { table, action } = rootInfo;
4171
+ if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
4172
+ const args = argsByName(node);
4173
+ const base2 = {
4174
+ kind: "mutation",
4175
+ op: "resolvedWrite",
4176
+ resolvedAction: action,
4177
+ alias,
4178
+ table,
4179
+ pinnedColumns: [],
4180
+ affectedRows: false,
4181
+ returning: this.lowerSelection(node.selectionSet, table)
4182
+ };
4183
+ switch (action) {
4184
+ case "insert": {
4185
+ assertKnownArgs(node, ["object"]);
4186
+ const object = args.get("object");
4187
+ if (!object) fail2(`'${name2}' needs object:`, "missing-argument");
4188
+ return { ...base2, objects: this.lowerInsertObjects(object.value, table, { list: false }) };
4189
+ }
4190
+ case "update": {
4191
+ assertKnownArgs(node, ["pkColumns", "_set"]);
4192
+ const pkColumns = args.get("pkColumns");
4193
+ if (!pkColumns || !args.get("_set")) fail2(`'${name2}' needs pkColumns: and _set:`, "missing-argument");
4194
+ return { ...base2, pk: this.lowerPkColumnsArg(pkColumns.value, table), ...this.lowerWriteValues(args, table) };
4195
+ }
4196
+ case "delete":
4197
+ return { ...base2, pk: this.lowerPkArgs(node, table) };
4198
+ case "revert": {
4199
+ assertKnownArgs(node, ["pkColumns", "columns"]);
4200
+ const pkColumns = args.get("pkColumns");
4201
+ const columns = args.get("columns");
4202
+ if (!pkColumns || !columns) fail2(`'${name2}' needs pkColumns: and columns:`, "missing-argument");
4203
+ const items = columns.value.kind === Kind.LIST ? columns.value.values : [columns.value];
4204
+ const meta = this.meta(table);
4205
+ const revertColumns = items.map((item) => {
4206
+ if (item.kind !== Kind.ENUM || !meta.settable.includes(item.value)) {
4207
+ fail2(`'${name2}' columns: must list update-column enum values written in the document`, "invalid-revert-columns", meta.settable);
4208
+ }
4209
+ return item.value;
4210
+ });
4211
+ if (revertColumns.length === 0) fail2(`'${name2}' columns: must name at least one column`, "invalid-revert-columns");
4212
+ return { ...base2, pk: this.lowerPkColumnsArg(pkColumns.value, table), revertColumns: [...new Set(revertColumns)] };
4213
+ }
4214
+ }
2978
4215
  }
2979
4216
  lowerMutationResponse(selectionSet, table) {
2980
4217
  let affectedRows = false;
2981
4218
  let returning = null;
2982
4219
  for (const node of selectionSet.selections) {
2983
- if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
2984
- const name = node.name.value;
2985
- if (name === "__typename") continue;
2986
- if (name === "affectedRows") {
4220
+ if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
4221
+ const name2 = node.name.value;
4222
+ if (name2 === "__typename") continue;
4223
+ if (name2 === "affectedRows") {
2987
4224
  affectedRows = true;
2988
4225
  continue;
2989
4226
  }
2990
- if (name === "returning") {
2991
- if (!node.selectionSet) fail("'returning' needs a selection set", "invalid-selection");
4227
+ if (name2 === "returning") {
4228
+ if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
2992
4229
  returning = this.lowerSelection(node.selectionSet, table);
2993
4230
  continue;
2994
4231
  }
2995
- fail(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
4232
+ fail2(`unknown mutation-response field '${name2}'`, "invalid-selection", ["affectedRows", "returning"]);
2996
4233
  }
2997
4234
  return { affectedRows, returning };
2998
4235
  }
2999
- lowerInsertObjects(node, table, { list }) {
4236
+ lowerInsertObjects(node, table, { list: list2 }) {
3000
4237
  if (node.kind === Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
3001
- if (list) {
3002
- if (node.kind !== Kind.LIST) fail(`objects: must be a list literal or a variable`, "invalid-insert");
4238
+ if (list2) {
4239
+ if (node.kind !== Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
3003
4240
  return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
3004
4241
  }
3005
4242
  return { rows: [this.lowerInsertRow(node, table)] };
3006
4243
  }
3007
4244
  lowerInsertRow(node, table) {
3008
- if (node.kind !== Kind.OBJECT) fail(`insert rows on '${table}' must be objects`, "invalid-insert");
4245
+ if (node.kind !== Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
3009
4246
  const meta = this.meta(table);
3010
4247
  return node.fields.map((field) => {
3011
4248
  const key = field.name.value;
3012
4249
  if (this.link(table, key)) {
3013
- fail(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
4250
+ fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
3014
4251
  }
3015
4252
  if (!meta.insertable.includes(key)) {
3016
4253
  const column = this.column(table, key);
3017
- if (column) fail(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
3018
- fail(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
4254
+ if (column) fail2(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
4255
+ fail2(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
3019
4256
  }
3020
4257
  return { column: key, value: this.lowerValue(field.value) };
3021
4258
  });
@@ -3024,32 +4261,32 @@ var OperationLowerer = class {
3024
4261
  if (!arg) return {};
3025
4262
  const node = arg.value;
3026
4263
  if (node.kind === Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
3027
- if (node.kind !== Kind.OBJECT) fail("onConflict: must be an object literal or a variable", "invalid-on-conflict");
4264
+ if (node.kind !== Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
3028
4265
  const meta = this.meta(table);
3029
4266
  let constraint;
3030
4267
  const updateColumns = [];
3031
4268
  for (const field of node.fields) {
3032
4269
  if (field.name.value === "constraint") {
3033
- if (field.value.kind !== Kind.ENUM) fail("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
4270
+ if (field.value.kind !== Kind.ENUM) fail2("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
3034
4271
  constraint = field.value.value;
3035
4272
  continue;
3036
4273
  }
3037
4274
  if (field.name.value === "updateColumns") {
3038
4275
  const items = field.value.kind === Kind.LIST ? field.value.values : [field.value];
3039
4276
  for (const item of items) {
3040
- if (item.kind !== Kind.ENUM) fail("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
4277
+ if (item.kind !== Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
3041
4278
  if (!meta.settable.includes(item.value)) {
3042
- fail(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
4279
+ fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
3043
4280
  }
3044
4281
  updateColumns.push(item.value);
3045
4282
  }
3046
4283
  continue;
3047
4284
  }
3048
- fail(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
4285
+ fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
3049
4286
  }
3050
- if (!constraint) fail("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
4287
+ if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
3051
4288
  if (!meta.constraints.some((c) => c.name === constraint)) {
3052
- fail(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
4289
+ fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
3053
4290
  }
3054
4291
  return { onConflict: { constraint, updateColumns } };
3055
4292
  }
@@ -3067,36 +4304,36 @@ var OperationLowerer = class {
3067
4304
  const inc = args.get("_inc");
3068
4305
  if (inc) {
3069
4306
  if (inc.value.kind === Kind.VARIABLE) {
3070
- fail("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
4307
+ fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
3071
4308
  }
3072
4309
  const numeric = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
3073
4310
  result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric, "_inc") };
3074
4311
  }
3075
4312
  if (!result.set && !result.append && !result.inc) {
3076
- fail(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
4313
+ fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
3077
4314
  }
3078
4315
  return result;
3079
4316
  }
3080
4317
  lowerColumnEntries(node, table, whitelist, argName) {
3081
- if (node.kind !== Kind.OBJECT) fail(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
4318
+ if (node.kind !== Kind.OBJECT) fail2(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
3082
4319
  return node.fields.map((field) => {
3083
4320
  const key = field.name.value;
3084
4321
  if (!whitelist.includes(key)) {
3085
- fail(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
4322
+ fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
3086
4323
  }
3087
4324
  return { column: key, value: this.lowerValue(field.value) };
3088
4325
  });
3089
4326
  }
3090
4327
  lowerPkColumnsArg(node, table) {
3091
- if (node.kind !== Kind.OBJECT) fail(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
4328
+ if (node.kind !== Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
3092
4329
  const meta = this.meta(table);
3093
4330
  const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
3094
4331
  for (const key of provided.keys()) {
3095
- if (!meta.primaryKey.includes(key)) fail(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
4332
+ if (!meta.primaryKey.includes(key)) fail2(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
3096
4333
  }
3097
4334
  return meta.primaryKey.map((column) => {
3098
4335
  const value = provided.get(column);
3099
- if (!value) fail(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
4336
+ if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
3100
4337
  return { column, value: this.lowerValue(value) };
3101
4338
  });
3102
4339
  }
@@ -3105,6 +4342,19 @@ function lowerOperation({ restricted, ctx }) {
3105
4342
  const lowerer = new OperationLowerer(ctx, restricted);
3106
4343
  return lowerer.lowerRoots(restricted.definition, restricted.kind);
3107
4344
  }
4345
+ function isCellColumn(meta, name2) {
4346
+ if (!meta.cells) return false;
4347
+ return Object.values(meta.cells).some((parts) => Object.values(parts).includes(name2));
4348
+ }
4349
+ function fieldNodes(selectionSet) {
4350
+ const out = [];
4351
+ for (const node of selectionSet.selections) {
4352
+ if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
4353
+ if (node.directives && node.directives.length > 0) fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
4354
+ if (node.name.value !== "__typename") out.push(node);
4355
+ }
4356
+ return out;
4357
+ }
3108
4358
  function parseSdl(sdl) {
3109
4359
  return buildSchema(sdl);
3110
4360
  }
@@ -3151,7 +4401,7 @@ function parseDocuments({
3151
4401
  }
3152
4402
  function unwrapType(node, variable) {
3153
4403
  let nullable = true;
3154
- let list = false;
4404
+ let list2 = false;
3155
4405
  let current = node;
3156
4406
  for (; ; ) {
3157
4407
  if (current.kind === Kind.NON_NULL_TYPE) {
@@ -3160,17 +4410,17 @@ function unwrapType(node, variable) {
3160
4410
  continue;
3161
4411
  }
3162
4412
  if (current.kind === Kind.LIST_TYPE) {
3163
- if (list) {
4413
+ if (list2) {
3164
4414
  throw new DocumentCompileError(`variable $${variable}: nested list types are not supported`, "unsupported-variable-type");
3165
4415
  }
3166
- list = true;
4416
+ list2 = true;
3167
4417
  current = current.type;
3168
4418
  continue;
3169
4419
  }
3170
- return { named: current.name.value, list, nullable };
4420
+ return { named: current.name.value, list: list2, nullable };
3171
4421
  }
3172
4422
  }
3173
- function classifyVariableType({ named, list }, variable, ctx) {
4423
+ function classifyVariableType({ named, list: list2 }, variable, ctx) {
3174
4424
  const inputRef = classifyInputType(named, ctx.tableByTypename);
3175
4425
  if (inputRef) {
3176
4426
  const listable = {
@@ -3183,13 +4433,13 @@ function classifyVariableType({ named, list }, variable, ctx) {
3183
4433
  insertInput: "either"
3184
4434
  };
3185
4435
  const rule = listable[inputRef.input];
3186
- if (rule === "must" && !list) {
4436
+ if (rule === "must" && !list2) {
3187
4437
  throw new DocumentCompileError(
3188
4438
  `variable $${variable}: ${named} must be used as a list (e.g. [${named}!])`,
3189
4439
  "unsupported-variable-type"
3190
4440
  );
3191
4441
  }
3192
- if (rule === "never" && list) {
4442
+ if (rule === "never" && list2) {
3193
4443
  throw new DocumentCompileError(
3194
4444
  `variable $${variable}: ${named} cannot be used as a list`,
3195
4445
  "unsupported-variable-type"
@@ -3226,7 +4476,7 @@ function restrictDefault({
3226
4476
  node,
3227
4477
  variable,
3228
4478
  type,
3229
- list,
4479
+ list: list2,
3230
4480
  nullable
3231
4481
  }) {
3232
4482
  if (type.kind === "inputRef") {
@@ -3246,7 +4496,7 @@ function restrictDefault({
3246
4496
  return { value: null };
3247
4497
  }
3248
4498
  if (Array.isArray(value)) {
3249
- if (!list) {
4499
+ if (!list2) {
3250
4500
  throw new DocumentCompileError(
3251
4501
  `variable $${variable}: default for a non-list variable must not be a list`,
3252
4502
  "unsupported-variable-default"
@@ -3254,7 +4504,7 @@ function restrictDefault({
3254
4504
  }
3255
4505
  return { value: value.map((item) => restrictDefaultLiteral(item, variable, type)) };
3256
4506
  }
3257
- if (list) {
4507
+ if (list2) {
3258
4508
  throw new DocumentCompileError(
3259
4509
  `variable $${variable}: default for a list variable must be a list`,
3260
4510
  "unsupported-variable-default"
@@ -3289,8 +4539,8 @@ function restrictOperation({
3289
4539
  if (operation.operation === "subscription") {
3290
4540
  throw new DocumentCompileError("subscriptions are not supported in v2.0 \u2014 poll a query instead", "subscription-unsupported");
3291
4541
  }
3292
- const name = operation.name?.value;
3293
- if (!name) {
4542
+ const name2 = operation.name?.value;
4543
+ if (!name2) {
3294
4544
  throw new DocumentCompileError(
3295
4545
  "anonymous operations are not supported \u2014 every operation must be named (the name keys the registry)",
3296
4546
  "anonymous-operation"
@@ -3299,14 +4549,14 @@ function restrictOperation({
3299
4549
  const variables = (operation.variableDefinitions ?? []).map((definition) => {
3300
4550
  const variable = definition.variable.name.value;
3301
4551
  const unwrapped = unwrapType(definition.type, variable);
3302
- const { list, nullable } = unwrapped;
4552
+ const { list: list2, nullable } = unwrapped;
3303
4553
  const type = classifyVariableType(unwrapped, variable, ctx);
3304
- const ir = { name: variable, type, nullable, list };
4554
+ const ir = { name: variable, type, nullable, list: list2 };
3305
4555
  if (definition.defaultValue === void 0) return ir;
3306
- return { ...ir, defaultValue: restrictDefault({ node: definition.defaultValue, variable, type, list, nullable }) };
4556
+ return { ...ir, defaultValue: restrictDefault({ node: definition.defaultValue, variable, type, list: list2, nullable }) };
3307
4557
  });
3308
4558
  return {
3309
- name,
4559
+ name: name2,
3310
4560
  kind: operation.operation === "mutation" ? "mutation" : "query",
3311
4561
  requires: parseRequires(operation),
3312
4562
  variables,
@@ -3344,8 +4594,20 @@ function compileNamespace({ canonical, sdl, documents }) {
3344
4594
  }
3345
4595
  const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
3346
4596
  const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
4597
+ for (const table of tableSet) {
4598
+ const meta = ctx.tables[table];
4599
+ if (meta.namespace && meta.namespace !== ctx.namespace && !meta.exportRead) {
4600
+ throw new DocumentCompileError(`object '${meta.namespace}/${table}' is not exported for reads`, "unexported-object");
4601
+ }
4602
+ }
4603
+ for (const root of roots) {
4604
+ const meta = ctx.tables[root.table];
4605
+ if (root.kind === "mutation" && meta.namespace && meta.namespace !== ctx.namespace && !meta.exportWrite) {
4606
+ throw new DocumentCompileError(`object '${meta.namespace}/${root.table}' is not exported for writes`, "unexported-object");
4607
+ }
4608
+ }
3347
4609
  operations[restricted.name] = {
3348
- irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
4610
+ irVersion: canonical.imports?.length ? COMPOSED_IR_VERSION : roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? GUARDED_IR_VERSION : LEGACY_IR_VERSION,
3349
4611
  name: restricted.name,
3350
4612
  opHash: computeOpHash({ definition: flattened, schemaHash }),
3351
4613
  kind: restricted.kind,
@@ -3372,9 +4634,14 @@ function compileNamespace({ canonical, sdl, documents }) {
3372
4634
  }
3373
4635
  if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
3374
4636
  const artifact = {
3375
- irVersion: requiredArtifactIrVersion(Object.values(operations)),
4637
+ irVersion: canonical.imports?.length ? COMPOSED_IR_VERSION : requiredArtifactIrVersion(Object.values(operations)),
3376
4638
  namespace: ctx.namespace,
3377
4639
  schemaHash,
4640
+ ownSchemaHash: computeSchemaHash(localContract(canonical)),
4641
+ ...canonical.imports?.length ? {
4642
+ dependencies: Object.fromEntries(canonical.imports.map((owner) => [owner.namespace, computeSchemaHash(localContract(owner))]))
4643
+ } : {},
4644
+ readPolicies: Object.fromEntries(ctx.readPolicies),
3378
4645
  opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
3379
4646
  tables: ctx.tables,
3380
4647
  operations
@@ -3437,9 +4704,10 @@ function allowedOps(type) {
3437
4704
  case "text":
3438
4705
  return ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike", "isNull"];
3439
4706
  case "uuid":
3440
- return ["eq", "neq", "in", "nin", "isNull"];
4707
+ return ["eq", "neq", "in", "nin", "like", "ilike", "isNull"];
3441
4708
  case "integer":
3442
4709
  case "bigint":
4710
+ return ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike", "isNull"];
3443
4711
  case "numeric":
3444
4712
  case "double":
3445
4713
  case "timestamptz":
@@ -3657,12 +4925,12 @@ function validateSelectColumns({ table, value }) {
3657
4925
  function validateInsertInput({
3658
4926
  table,
3659
4927
  value,
3660
- list,
4928
+ list: list2,
3661
4929
  maxRows
3662
4930
  }) {
3663
4931
  const collector = new IssueCollector();
3664
4932
  const rowCap = maxRows ?? 1e3;
3665
- const rows = list ? value : [value];
4933
+ const rows = list2 ? value : [value];
3666
4934
  if (!Array.isArray(rows)) {
3667
4935
  collector.add("invalid-shape", "$", "objects must be an array of rows");
3668
4936
  return { ok: false, issues: collector.issues };
@@ -3673,7 +4941,7 @@ function validateInsertInput({
3673
4941
  }
3674
4942
  const normalized = [];
3675
4943
  rows.forEach((row, index) => {
3676
- const path = list ? `$[${index}]` : "$";
4944
+ const path = list2 ? `$[${index}]` : "$";
3677
4945
  if (!isPlainObject(row)) {
3678
4946
  collector.add("invalid-shape", path, "insert rows must be objects");
3679
4947
  return;
@@ -3783,6 +5051,744 @@ function validateOnConflictInput({
3783
5051
  return finish(collector, { constraint, updateColumns });
3784
5052
  }
3785
5053
 
3786
- export { COMPILER_MAJOR, CONST_TRUE, DEFAULT_LIMIT, DocumentCompileError, MAX_LIMIT, NamespaceCompileError, ORDER_BY_DIRECTIONS, OperationLowerer, PLATFORM_USER_TABLE, PLATFORM_USER_TYPENAME, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, PolicyCompileError, PolicyParseError, allowedBoolExpOperators, allowedOps, andPred, assertClaimsOnlyWhen, buildCompileContext, camelCase, canonicalOperationText, classifyInputType, collectPinnedColumns, compileNamespace, compileNamespaceOrThrow, compilePolicy, compilePredicateV2, computeOpHash, computeOpsHash, computePinnedColumnsByTable, computeSchemaHash, enumTypeName, flattenOperation, injectPolicies, inputTypeName, lowerOperation, mutationRootNames, parseDocuments, parsePolicy, parseSdl, pascalCase, platformUserMeta, predContains, predContainsExists, queryRootNames, restrictOperation, typeNameForTable, validateAppendInput, validateBoolExpInput, validateInsertInput, validateOnConflictInput, validateOrderByInput, validateSelectColumns, validateSetInput, walkPolicy, walkPred };
5054
+ // src/engine/types.ts
5055
+ var SQL_TYPE_NAMES = {
5056
+ double: "double precision",
5057
+ timestamptz: "timestamp with time zone"
5058
+ };
5059
+ var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
5060
+
5061
+ // src/compiler/pred-ir-sql.ts
5062
+ function compilePredIrToSql(pred, options = {}) {
5063
+ switch (pred.kind) {
5064
+ case "const":
5065
+ return pred.value ? "TRUE" : "FALSE";
5066
+ case "and":
5067
+ return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" AND ")})`;
5068
+ case "or":
5069
+ return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" OR ")})`;
5070
+ case "not":
5071
+ return `(NOT ${compilePredIrToSql(pred.operand, options)})`;
5072
+ case "cmp":
5073
+ return compileCmp(pred.op, pred.column, pred.value, options);
5074
+ case "capability":
5075
+ return `(${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
5076
+ case "role":
5077
+ return `(${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
5078
+ case "exists":
5079
+ if (options.exists) return options.exists(pred);
5080
+ throw new OrmDefinitionError("synced view read policy cannot use 'exists' without a relationship resolver");
5081
+ case "aggPredicate":
5082
+ case "dynamicFilter":
5083
+ throw new OrmDefinitionError(
5084
+ `synced view read policy cannot use '${pred.kind}' \u2014 use row filters and claim/capability/role checks only`
5085
+ );
5086
+ default: {
5087
+ const exhausted = pred;
5088
+ throw new OrmDefinitionError(`unsupported synced view predicate kind '${String(exhausted)}'`);
5089
+ }
5090
+ }
5091
+ }
5092
+ function physicalColumn(logical, options) {
5093
+ return options.columnNames?.[logical] ?? logical;
5094
+ }
5095
+ function compileCmp(op, column, value, options) {
5096
+ const physical = physicalColumn(column, options);
5097
+ const declared = options.columnTypes?.[physical] ?? options.columnTypes?.[column];
5098
+ const isEnum = typeof declared === "object" && declared !== null;
5099
+ const qualified = options.alias ? `${quoteIdentifier(options.alias)}.${quoteIdentifier(physical)}` : quoteIdentifier(physical);
5100
+ const col = isEnum ? `${qualified}::text` : qualified;
5101
+ const columnType = isEnum ? "text" : declared;
5102
+ switch (op) {
5103
+ case "eq":
5104
+ return compileEquality(col, value, columnType, false);
5105
+ case "neq":
5106
+ return compileEquality(col, value, columnType, true);
5107
+ case "gt":
5108
+ return `${col} > ${compileValue(value, columnType)}`;
5109
+ case "gte":
5110
+ return `${col} >= ${compileValue(value, columnType)}`;
5111
+ case "lt":
5112
+ return `${col} < ${compileValue(value, columnType)}`;
5113
+ case "lte":
5114
+ return `${col} <= ${compileValue(value, columnType)}`;
5115
+ case "in":
5116
+ return `${col} = ANY(${compileArrayValue(value, columnType)})`;
5117
+ case "nin":
5118
+ return `${col} <> ALL(${compileArrayValue(value, columnType)})`;
5119
+ case "like":
5120
+ return `${col} LIKE ${compileValue(value, columnType)}`;
5121
+ case "ilike":
5122
+ return `${col} ILIKE ${compileValue(value, columnType)}`;
5123
+ case "isNull":
5124
+ return value.kind === "literal" && value.value === true ? `${col} IS NULL` : `${col} IS NOT NULL`;
5125
+ case "contains":
5126
+ case "hasKey":
5127
+ throw new OrmDefinitionError(`synced view read policy cannot use json operator '${op}'`);
5128
+ default: {
5129
+ const exhausted = op;
5130
+ throw new OrmDefinitionError(`unsupported synced view comparison '${String(exhausted)}'`);
5131
+ }
5132
+ }
5133
+ }
5134
+ function compileEquality(col, value, columnType, negated, options) {
5135
+ if (value.kind === "literal" && value.value === null) {
5136
+ return negated ? `${col} IS NOT NULL` : `${col} IS NULL`;
5137
+ }
5138
+ const rhs = compileValue(value, columnType);
5139
+ return negated ? `${col} <> ${rhs}` : `${col} = ${rhs}`;
5140
+ }
5141
+ function compileValue(value, columnType, _options) {
5142
+ switch (value.kind) {
5143
+ case "literal": {
5144
+ const literal = value.value;
5145
+ if (literal === null) return "NULL";
5146
+ if (typeof literal === "string") return quoteLiteral(literal);
5147
+ if (typeof literal === "boolean") return literal ? "TRUE" : "FALSE";
5148
+ return String(literal);
5149
+ }
5150
+ case "claim": {
5151
+ const expression = claimExpression(value.key);
5152
+ return columnType && columnType !== "text" ? `(${expression})::${sqlTypeName(columnType)}` : expression;
5153
+ }
5154
+ case "variable":
5155
+ throw new OrmDefinitionError("synced view read policy cannot reference operation variables");
5156
+ default: {
5157
+ const exhausted = value;
5158
+ throw new OrmDefinitionError(`unsupported synced view value kind '${String(exhausted)}'`);
5159
+ }
5160
+ }
5161
+ }
5162
+ function compileArrayValue(value, columnType, _options) {
5163
+ if (value.kind !== "literal" || !Array.isArray(value.value)) {
5164
+ throw new OrmDefinitionError("synced view IN/NIN comparisons require literal arrays");
5165
+ }
5166
+ const entries = value.value.map((entry) => {
5167
+ if (entry === null) return "NULL";
5168
+ if (typeof entry === "string") return quoteLiteral(entry);
5169
+ if (typeof entry === "boolean") return entry ? "TRUE" : "FALSE";
5170
+ return String(entry);
5171
+ });
5172
+ const array = `ARRAY[${entries.join(", ")}]`;
5173
+ return columnType ? `${array}::${sqlTypeName(columnType)}[]` : array;
5174
+ }
5175
+
5176
+ // src/sql/catalog.ts
5177
+ function sqlHash(value) {
5178
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
5179
+ }
5180
+ function createSqlCatalog({ canonical, publishedObjects, executionTarget }) {
5181
+ if (!publishedObjects.length || new Set(publishedObjects).size !== publishedObjects.length) throw new Error("SQL objects must be explicitly declared and unique");
5182
+ const { ctx, diagnostics } = buildCompileContext(canonical);
5183
+ if (diagnostics.some((d) => d.severity === "error")) throw new Error(diagnostics.map((d) => d.message).join("; "));
5184
+ const identity = sqlHash(`ontology-sql-v1\0${computeSchemaHash(canonical)}\0${executionTarget}`);
5185
+ const refOf = (meta) => `${meta.namespace ?? canonical.namespace}/${meta.table}`;
5186
+ const definitions = [canonical, ...canonical.imports ?? []];
5187
+ const internal = new Set(definitions.flatMap((owner) => [
5188
+ ...owner.tables.filter((t) => t.internal).map((t) => `${owner.namespace}/${t.table}`),
5189
+ ...(owner.views ?? []).filter((v) => v.internal).map((v) => `${owner.namespace}/${v.view}`)
5190
+ ]));
5191
+ const objects = publishedObjects.map((ref) => {
5192
+ const meta = Object.values(ctx.tables).find((table) => refOf(table) === ref);
5193
+ if (!meta || internal.has(ref) || meta.namespace !== canonical.namespace && !meta.exportRead) throw new Error(`SQL object not published: ${ref}`);
5194
+ const reads = /* @__PURE__ */ new Set([ref]);
5195
+ let sequence = 0;
5196
+ const predicate = (table, pred, alias) => compilePredIrToSql(pred, {
5197
+ alias,
5198
+ namespace: table.namespace ?? canonical.namespace,
5199
+ columnNames: Object.fromEntries(table.columns.map((c) => [c.name, c.columnName])),
5200
+ columnTypes: Object.fromEntries(table.columns.map((c) => [c.name, c.type])),
5201
+ exists: (node) => {
5202
+ const link = table.links.find((candidate) => candidate.name === node.link);
5203
+ const target = link?.target ? ctx.tables[link.target] : void 0;
5204
+ if (!link || !target) throw new Error("SQL policy link is unavailable");
5205
+ reads.add(refOf(target));
5206
+ const child = `p${++sequence}`;
5207
+ const col = (t, a, name2) => {
5208
+ const c = t.columns.find((c2) => c2.name === name2 || c2.columnName === name2);
5209
+ if (!c) throw new Error("SQL policy column is unavailable");
5210
+ return `${quoteIdentifier(a)}.${quoteIdentifier(c.columnName)}`;
5211
+ };
5212
+ const join = link.kind === "toMany" ? `${col(target, child, link.on)} = ${col(table, alias, link.references)}` : `${col(table, alias, link.on)} = ${col(target, child, link.references)}`;
5213
+ return `EXISTS (SELECT 1 FROM ${quoteIdentifier(target.schema)}.${quoteIdentifier(target.table)} AS ${quoteIdentifier(child)} WHERE ${join} AND (${predicate(target, node.where, child)}))`;
5214
+ }
5215
+ });
5216
+ const columns = meta.columns.filter((c) => meta.filterable.includes(c.name)).map((c) => ({
5217
+ name: c.name,
5218
+ type: typeof c.type === "string" ? c.type : "text",
5219
+ nullable: c.nullable
5220
+ }));
5221
+ const projection = columns.map((c) => {
5222
+ const physical = meta.columns.find((m) => m.name === c.name);
5223
+ return `${quoteIdentifier("p0")}.${quoteIdentifier(physical.columnName)}${typeof physical.type === "object" ? "::text" : ""} AS ${quoteIdentifier(c.name)}`;
5224
+ }).join(", ");
5225
+ const policy = ctx.readPolicies.get(meta.table) ?? { kind: "const", value: true };
5226
+ const scan = `SELECT ${projection} FROM ${quoteIdentifier(meta.schema)}.${quoteIdentifier(meta.table)} AS ${quoteIdentifier("p0")} WHERE ${predicate(meta, policy, "p0")}`;
5227
+ return { ref, table: meta.table, columns, scan, read: [...reads].sort() };
5228
+ });
5229
+ return { identity, objects };
5230
+ }
5231
+
5232
+ // src/sql/ast.ts
5233
+ function obj(value) {
5234
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("unsupported SQL node");
5235
+ return value;
5236
+ }
5237
+ function list(value) {
5238
+ return value === void 0 ? [] : Array.isArray(value) ? value : fail3("expected SQL list");
5239
+ }
5240
+ function str(value) {
5241
+ return typeof value === "string" ? value : fail3("expected SQL name");
5242
+ }
5243
+ function fail3(message) {
5244
+ throw new Error(message);
5245
+ }
5246
+ function strings(value) {
5247
+ return list(value).map((v) => str(obj(obj(v).String).sval));
5248
+ }
5249
+ function only(node, keys) {
5250
+ for (const [key, value] of Object.entries(node)) {
5251
+ if (!keys.includes(key) && value !== void 0 && value !== null && value !== false && value !== "" && value !== 0 && !(Array.isArray(value) && value.length === 0)) {
5252
+ fail3(`unsupported SQL clause: ${key}`);
5253
+ }
5254
+ }
5255
+ }
5256
+
5257
+ // src/sql/compiler.ts
5258
+ var parser;
5259
+ var SQL_TYPES = ["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"];
5260
+ var pgType = (type) => type === "double" ? "double precision" : SQL_TYPES.some((t) => t === type) ? type : fail3("unsupported SQL type");
5261
+ async function compileOntologySql({ catalog, sql, parameterTypes = [], maxRows = 1e3 }) {
5262
+ if (!Number.isInteger(maxRows) || maxRows < 1 || maxRows > 1e4 || Buffer.byteLength(sql) > 65536) fail3("SQL limit exceeded");
5263
+ parser ??= new PgParser({ version: 17 });
5264
+ const parsed = await parser.parse(sql);
5265
+ if (parsed.error || !parsed.tree) fail3("invalid SQL");
5266
+ const statements = list(obj(parsed.tree).stmts);
5267
+ if (statements.length !== 1) fail3("one SQL SELECT is required");
5268
+ const emitter = new SqlEmitter({ catalog, parameterTypes });
5269
+ const result = emitter.select(obj(obj(statements[0]).stmt), { relations: [], ctes: /* @__PURE__ */ new Map() });
5270
+ if (emitter.used.size !== parameterTypes.length) fail3("SQL parameters must be used exactly as declared");
5271
+ if (!emitter.read.size) fail3("SQL operation must read an ontology object");
5272
+ return {
5273
+ ...result,
5274
+ read: [...emitter.read].sort(),
5275
+ maxRows,
5276
+ queryHash: sqlHash(JSON.stringify(["ontology-sql-v1", catalog.identity, result.text, parameterTypes, maxRows]))
5277
+ };
5278
+ }
5279
+ var SqlEmitter = class {
5280
+ constructor(input) {
5281
+ this.input = input;
5282
+ }
5283
+ read = /* @__PURE__ */ new Set();
5284
+ used = /* @__PURE__ */ new Set();
5285
+ nodes = 0;
5286
+ depth = 0;
5287
+ budget() {
5288
+ if (++this.nodes > 5e3 || this.depth > 48) fail3("SQL complexity limit exceeded");
5289
+ }
5290
+ select(wrapper, parent, resolveUnknown = true) {
5291
+ this.budget();
5292
+ this.depth++;
5293
+ const node = obj(wrapper.SelectStmt);
5294
+ only(node, ["targetList", "fromClause", "whereClause", "groupClause", "havingClause", "sortClause", "limitCount", "limitOffset", "limitOption", "op", "withClause", "all", "larg", "rarg"]);
5295
+ if (!["SETOP_NONE", "SETOP_UNION"].includes(str(node.op))) fail3("unsupported SQL set operation");
5296
+ if (node.op === "SETOP_NONE" && (node.all || node.larg || node.rarg)) fail3("invalid SQL set operation");
5297
+ if (node.limitOption && !["LIMIT_OPTION_DEFAULT", "LIMIT_OPTION_COUNT"].includes(str(node.limitOption))) fail3("unsupported SQL limit");
5298
+ const scope = { relations: [], parent, ctes: new Map(parent.ctes) };
5299
+ const ctes = [];
5300
+ if (node.withClause) {
5301
+ const withNode = obj(node.withClause);
5302
+ only(withNode, ["ctes", "location"]);
5303
+ for (const entry of list(withNode.ctes)) {
5304
+ const cte = obj(obj(entry).CommonTableExpr);
5305
+ only(cte, ["ctename", "ctequery", "ctematerialized", "location"]);
5306
+ const name2 = str(cte.ctename);
5307
+ if (scope.ctes.has(name2)) fail3("duplicate CTE");
5308
+ const result2 = this.select(obj(cte.ctequery), { ...parent, ctes: scope.ctes });
5309
+ scope.ctes.set(name2, result2);
5310
+ ctes.push(`${quoteIdentifier(name2)} AS (${result2.text})`);
5311
+ }
5312
+ }
5313
+ const result = node.op === "SETOP_UNION" ? this.union(node, scope) : this.projection(node, scope);
5314
+ const { columns } = result;
5315
+ let text = `${ctes.length ? `WITH ${ctes.join(", ")} ` : ""}${result.text}`;
5316
+ if (list(node.sortClause).length) text += ` ORDER BY ${list(node.sortClause).map((n) => {
5317
+ const sort = obj(obj(n).SortBy);
5318
+ only(sort, ["node", "sortby_dir", "sortby_nulls", "location"]);
5319
+ const direction = sort.sortby_dir === "SORTBY_DESC" ? "DESC" : sort.sortby_dir === "SORTBY_ASC" || sort.sortby_dir === "SORTBY_DEFAULT" ? "ASC" : fail3("unsupported SQL sort");
5320
+ const nulls = sort.sortby_nulls === "SORTBY_NULLS_FIRST" ? " NULLS FIRST" : sort.sortby_nulls === "SORTBY_NULLS_LAST" ? " NULLS LAST" : "";
5321
+ const value = obj(sort.node);
5322
+ const reference = value.ColumnRef ? strings(obj(value.ColumnRef).fields) : [];
5323
+ const projected = reference.length === 1 ? columns.filter((c) => c.name === reference[0]) : [];
5324
+ return `${projected.length === 1 ? quoteIdentifier(projected[0].name) : this.expr(value, scope).text} ${direction}${nulls}`;
5325
+ }).join(", ")}`;
5326
+ for (const [key, keyword] of [["limitCount", "LIMIT"], ["limitOffset", "OFFSET"]]) {
5327
+ if (node[key]) text += ` ${keyword} ${this.expr(obj(node[key]), scope).text}`;
5328
+ }
5329
+ this.depth--;
5330
+ return { text, columns: resolveUnknown ? columns.map((c) => c.type === "unknown" ? { ...c, type: "text" } : c) : columns };
5331
+ }
5332
+ union(node, scope) {
5333
+ for (const key of ["targetList", "fromClause", "whereClause", "groupClause", "havingClause"]) {
5334
+ if (node[key]) fail3("unsupported UNION clause");
5335
+ }
5336
+ const left = this.select({ SelectStmt: obj(node.larg) }, scope, false);
5337
+ const right = this.select({ SelectStmt: obj(node.rarg) }, scope, false);
5338
+ if (left.columns.length !== right.columns.length) fail3("UNION branches must have the same column count");
5339
+ const numeric = ["integer", "bigint", "numeric", "double"];
5340
+ const columns = left.columns.map((column, index) => {
5341
+ const other = right.columns[index];
5342
+ const type = column.type === "unknown" && other.type === "unknown" ? "text" : column.type === "unknown" ? other.type : other.type === "unknown" ? column.type : column.type === other.type ? column.type : numeric.includes(column.type) && numeric.includes(other.type) ? numeric[Math.max(numeric.indexOf(column.type), numeric.indexOf(other.type))] : fail3("UNION columns require compatible types; use explicit casts");
5343
+ return { name: column.name, type, nullable: column.nullable || other.nullable };
5344
+ });
5345
+ return { text: `(${left.text}) UNION${node.all ? " ALL" : ""} (${right.text})`, columns };
5346
+ }
5347
+ projection(node, scope) {
5348
+ const from = list(node.fromClause).map((n) => this.from(obj(n), scope)).join(", ");
5349
+ const columns = [];
5350
+ const targets = [];
5351
+ for (const item of list(node.targetList)) {
5352
+ const target = obj(obj(item).ResTarget);
5353
+ only(target, ["name", "val", "location"]);
5354
+ const value = obj(target.val);
5355
+ const fields = value.ColumnRef ? list(obj(value.ColumnRef).fields) : [];
5356
+ if (fields.length && obj(fields[fields.length - 1]).A_Star) {
5357
+ const matches = fields.length === 1 ? scope.relations : scope.relations.filter((r) => r.alias === strings(fields.slice(0, -1))[0]);
5358
+ if (!matches.length || fields.length > 2 || target.name) fail3("invalid wildcard");
5359
+ for (const relation of matches) for (const column of relation.columns) {
5360
+ columns.push(column);
5361
+ targets.push(`${quoteIdentifier(relation.alias)}.${quoteIdentifier(column.name)}`);
5362
+ }
5363
+ } else {
5364
+ const expression = this.expr(value, scope);
5365
+ const name2 = typeof target.name === "string" && target.name ? target.name : expression.name ?? `column${columns.length + 1}`;
5366
+ columns.push({ name: name2, type: expression.type, nullable: true });
5367
+ targets.push(`${expression.text} AS ${quoteIdentifier(name2)}`);
5368
+ }
5369
+ }
5370
+ if (!targets.length || targets.length > 256) fail3("invalid SQL projection");
5371
+ let text = `SELECT ${targets.join(", ")}${from ? ` FROM ${from}` : ""}`;
5372
+ if (node.whereClause) text += ` WHERE ${this.expr(obj(node.whereClause), scope).text}`;
5373
+ if (list(node.groupClause).length) text += ` GROUP BY ${list(node.groupClause).map((n) => this.expr(obj(n), scope).text).join(", ")}`;
5374
+ if (node.havingClause) text += ` HAVING ${this.expr(obj(node.havingClause), scope).text}`;
5375
+ return { text, columns };
5376
+ }
5377
+ from(node, scope) {
5378
+ this.budget();
5379
+ this.depth++;
5380
+ try {
5381
+ return this.relation(node, scope);
5382
+ } finally {
5383
+ this.depth--;
5384
+ }
5385
+ }
5386
+ relation(node, scope) {
5387
+ if (node.RangeVar) {
5388
+ const range = obj(node.RangeVar);
5389
+ only(range, ["schemaname", "relname", "inh", "relpersistence", "alias", "location"]);
5390
+ if (range.inh !== true) fail3("ONLY is not supported for ontology objects");
5391
+ const name2 = str(range.relname);
5392
+ const alias = range.alias ? str(obj(range.alias).aliasname) : name2;
5393
+ if (range.alias) only(obj(range.alias), ["aliasname"]);
5394
+ if (scope.relations.some((r) => r.alias === alias)) fail3("duplicate SQL alias");
5395
+ const cte = !range.schemaname ? scope.ctes.get(name2) : void 0;
5396
+ if (cte) {
5397
+ scope.relations.push({ alias, columns: cte.columns });
5398
+ return `${quoteIdentifier(name2)} AS ${quoteIdentifier(alias)}`;
5399
+ }
5400
+ const object = this.input.catalog.objects.find((o) => o.ref === `${range.schemaname}/${name2}`);
5401
+ if (!object) fail3("SQL object is not declared or available");
5402
+ for (const ref of object.read) this.read.add(ref);
5403
+ scope.relations.push({ alias, columns: object.columns });
5404
+ const source = `(WITH ${quoteIdentifier("__orm_scan")} AS MATERIALIZED (${object.scan}) SELECT * FROM ${quoteIdentifier("__orm_scan")})`;
5405
+ return `${source} AS ${quoteIdentifier(alias)}`;
5406
+ }
5407
+ if (node.JoinExpr) {
5408
+ const join = obj(node.JoinExpr);
5409
+ only(join, ["jointype", "larg", "rarg", "quals"]);
5410
+ if (!["JOIN_INNER", "JOIN_LEFT", "JOIN_RIGHT", "JOIN_FULL"].includes(str(join.jointype))) fail3("unsupported SQL join");
5411
+ const start = scope.relations.length;
5412
+ const left = this.from(obj(join.larg), scope);
5413
+ const middle = scope.relations.length;
5414
+ const right = this.from(obj(join.rarg), scope);
5415
+ const nullable = (relations) => {
5416
+ for (const relation of relations) relation.columns = relation.columns.map((c) => ({ ...c, nullable: true }));
5417
+ };
5418
+ if (["JOIN_RIGHT", "JOIN_FULL"].includes(str(join.jointype))) nullable(scope.relations.slice(start, middle));
5419
+ if (["JOIN_LEFT", "JOIN_FULL"].includes(str(join.jointype))) nullable(scope.relations.slice(middle));
5420
+ const kind = str(join.jointype).slice("JOIN_".length);
5421
+ return `(${left} ${kind} JOIN ${right} ON ${join.quals ? this.expr(obj(join.quals), scope).text : "TRUE"})`;
5422
+ }
5423
+ if (node.RangeSubselect) {
5424
+ const range = obj(node.RangeSubselect);
5425
+ only(range, ["subquery", "alias"]);
5426
+ const alias = str(obj(range.alias).aliasname);
5427
+ only(obj(range.alias), ["aliasname"]);
5428
+ if (scope.relations.some((r) => r.alias === alias)) fail3("duplicate SQL alias");
5429
+ const result = this.select(obj(range.subquery), { relations: [], ctes: scope.ctes });
5430
+ scope.relations.push({ alias, columns: result.columns });
5431
+ return `(${result.text}) AS ${quoteIdentifier(alias)}`;
5432
+ }
5433
+ return fail3("unsupported SQL relation");
5434
+ }
5435
+ column(fields, scope) {
5436
+ if (fields.length !== 1 && fields.length !== 2) fail3("unsupported column reference");
5437
+ const name2 = fields[fields.length - 1];
5438
+ const matches = scope.relations.flatMap((r) => fields.length === 2 && r.alias !== fields[0] ? [] : r.columns.filter((c) => c.name === name2).map((c) => ({ r, c })));
5439
+ if (matches.length > 1) fail3("ambiguous SQL column");
5440
+ if (matches.length === 1) return { text: `${quoteIdentifier(matches[0].r.alias)}.${quoteIdentifier(name2)}`, type: matches[0].c.type, name: name2 };
5441
+ if (scope.parent && !(fields.length === 2 && scope.relations.some((r) => r.alias === fields[0]))) return this.column(fields, scope.parent);
5442
+ return fail3("unknown ontology property");
5443
+ }
5444
+ expr(node, scope) {
5445
+ this.budget();
5446
+ this.depth++;
5447
+ try {
5448
+ return this.expression(node, scope);
5449
+ } finally {
5450
+ this.depth--;
5451
+ }
5452
+ }
5453
+ expression(node, scope) {
5454
+ if (node.ColumnRef) return this.column(strings(obj(node.ColumnRef).fields), scope);
5455
+ if (node.ParamRef) {
5456
+ const number = obj(node.ParamRef).number;
5457
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number > this.input.parameterTypes.length) fail3("undeclared SQL parameter");
5458
+ this.used.add(number);
5459
+ const type = this.input.parameterTypes[number - 1];
5460
+ return { text: `$${number}::${pgType(type)}`, type };
5461
+ }
5462
+ if (node.A_Const) {
5463
+ const literal = obj(node.A_Const);
5464
+ if (literal.isnull) return { text: "NULL", type: "unknown" };
5465
+ if (literal.sval) return { text: quoteLiteral(str(obj(literal.sval).sval)), type: "unknown" };
5466
+ if (literal.boolval) return { text: obj(literal.boolval).boolval ? "TRUE" : "FALSE", type: "boolean" };
5467
+ const value = literal.ival ? obj(literal.ival).ival : obj(literal.fval).fval;
5468
+ if (!/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(String(value))) fail3("unsupported SQL constant");
5469
+ return { text: String(value), type: literal.ival ? "integer" : "numeric" };
5470
+ }
5471
+ if (node.A_Expr) {
5472
+ const value = obj(node.A_Expr);
5473
+ only(value, ["kind", "name", "lexpr", "rexpr", "location"]);
5474
+ const names = strings(value.name);
5475
+ if (names.length !== 1 || value.kind !== "AEXPR_OP") fail3("unsupported SQL operator");
5476
+ const operator = names[0];
5477
+ if (!["=", "<>", "!=", "<", ">", "<=", ">=", "+", "-", "*", "/"].includes(operator)) fail3("unsupported SQL operator");
5478
+ const left = this.expr(obj(value.lexpr), scope), right = this.expr(obj(value.rexpr), scope);
5479
+ const numericTypes = ["integer", "bigint", "numeric", "double"];
5480
+ const arithmetic = ["+", "-", "*", "/"].includes(operator);
5481
+ if (arithmetic && (!numericTypes.includes(left.type) || !numericTypes.includes(right.type))) fail3("arithmetic requires numeric operands");
5482
+ const type = arithmetic ? numericTypes[Math.max(numericTypes.indexOf(left.type), numericTypes.indexOf(right.type))] : "boolean";
5483
+ return { text: `(${left.text} OPERATOR(pg_catalog.${operator}) ${right.text})`, type };
5484
+ }
5485
+ if (node.BoolExpr) {
5486
+ const value = obj(node.BoolExpr);
5487
+ const args = list(value.args).map((a) => this.expr(obj(a), scope).text);
5488
+ if (value.boolop === "NOT_EXPR" && args.length === 1) return { text: `(NOT ${args[0]})`, type: "boolean" };
5489
+ if (!["AND_EXPR", "OR_EXPR"].includes(str(value.boolop))) fail3("unsupported boolean expression");
5490
+ return { text: `(${args.join(value.boolop === "AND_EXPR" ? " AND " : " OR ")})`, type: "boolean" };
5491
+ }
5492
+ if (node.NullTest) {
5493
+ const value = obj(node.NullTest);
5494
+ only(value, ["arg", "nulltesttype", "location"]);
5495
+ if (!["IS_NULL", "IS_NOT_NULL"].includes(str(value.nulltesttype))) fail3("invalid null comparison");
5496
+ return { text: `(${this.expr(obj(value.arg), scope).text} IS ${value.nulltesttype === "IS_NOT_NULL" ? "NOT " : ""}NULL)`, type: "boolean" };
5497
+ }
5498
+ if (node.SubLink) {
5499
+ const value = obj(node.SubLink);
5500
+ only(value, ["subLinkType", "subselect", "location"]);
5501
+ if (value.subLinkType !== "EXISTS_SUBLINK") fail3("only EXISTS subqueries are supported");
5502
+ return { text: `EXISTS (${this.select(obj(value.subselect), scope).text})`, type: "boolean" };
5503
+ }
5504
+ if (node.TypeCast) {
5505
+ const value = obj(node.TypeCast), typeName2 = obj(value.typeName);
5506
+ only(typeName2, ["names", "typemod", "location"]);
5507
+ const names = strings(typeName2.names);
5508
+ if (names.length > 2 || names.length === 2 && names[0] !== "pg_catalog") fail3("unsupported SQL cast");
5509
+ const aliases = { int4: "integer", int8: "bigint", float8: "double", bool: "boolean" };
5510
+ const name2 = names[names.length - 1], type = aliases[name2] ?? name2;
5511
+ return { text: `(${this.expr(obj(value.arg), scope).text})::${pgType(type)}`, type };
5512
+ }
5513
+ if (node.FuncCall) {
5514
+ const value = obj(node.FuncCall);
5515
+ only(value, ["funcname", "args", "agg_star", "agg_distinct", "funcformat", "location"]);
5516
+ const names = strings(value.funcname);
5517
+ if (names.length !== 1) fail3("SQL function qualification is not supported");
5518
+ const name2 = names[0];
5519
+ if (!["count", "sum", "avg", "min", "max", "lower", "upper", "length"].includes(name2)) fail3("SQL function is not allowed");
5520
+ const args = list(value.args).map((a) => this.expr(obj(a), scope));
5521
+ if (value.agg_star && (name2 !== "count" || args.length) || !value.agg_star && args.length !== 1) fail3("unsupported SQL function arguments");
5522
+ const argType = args[0]?.type ?? "integer";
5523
+ if (["lower", "upper", "length"].includes(name2) && !["text", "unknown"].includes(argType)) fail3("text function requires text");
5524
+ if (["sum", "avg"].includes(name2) && !["integer", "bigint", "numeric", "double"].includes(argType)) fail3("aggregate requires numeric input");
5525
+ const type = name2 === "count" ? "bigint" : name2 === "length" ? "integer" : ["lower", "upper"].includes(name2) ? "text" : name2 === "avg" ? argType === "double" ? "double" : "numeric" : name2 === "sum" ? argType === "integer" ? "bigint" : argType === "bigint" ? "numeric" : argType : argType;
5526
+ return { text: `pg_catalog.${name2}(${value.agg_distinct ? "DISTINCT " : ""}${value.agg_star ? "*" : args.map((a) => a.text).join(", ")})`, type, name: name2 };
5527
+ }
5528
+ return fail3(`unsupported SQL expression: ${Object.keys(node).join(",")}`);
5529
+ }
5530
+ };
5531
+ var sqlScalarSchema = z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]);
5532
+ var name = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(63).refine((value) => !["__proto__", "prototype", "constructor"].includes(value), "reserved name");
5533
+ var base = z.object({
5534
+ name,
5535
+ namespace: name,
5536
+ language: z.literal("sql"),
5537
+ kind: z.literal("query"),
5538
+ requires: z.object({ capability: z.string().min(1).max(128) }).strict(),
5539
+ ontology: z.object({ read: z.array(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*\/[A-Za-z_][A-Za-z0-9_]*$/)).min(1).max(256) }).strict(),
5540
+ maxRows: z.number().int().min(1).max(1e4).default(1e3)
5541
+ });
5542
+ var fixed = base.extend({
5543
+ parameters: z.record(name, z.object({ type: sqlScalarSchema, required: z.boolean().default(true) }).strict()).default({}),
5544
+ sql: z.string().min(1).max(65536)
5545
+ }).strict();
5546
+ var runtime = base.extend({ mode: z.literal("runtime") }).strict();
5547
+ var sqlOperationDefinitionSchema = z.union([fixed, runtime]);
5548
+ function defineOperation(input) {
5549
+ return sqlOperationDefinitionSchema.parse(input);
5550
+ }
5551
+
5552
+ // src/sql/parameters.ts
5553
+ function bindNamedParameters({ sql, names }) {
5554
+ let result = "", i = 0;
5555
+ while (i < sql.length) {
5556
+ const start = i;
5557
+ if (sql.startsWith("--", i)) {
5558
+ i = sql.indexOf("\n", i);
5559
+ if (i < 0) i = sql.length;
5560
+ } else if (sql.startsWith("/*", i)) {
5561
+ i += 2;
5562
+ let depth = 1;
5563
+ while (i < sql.length && depth) {
5564
+ if (sql.startsWith("/*", i)) {
5565
+ depth++;
5566
+ i += 2;
5567
+ } else if (sql.startsWith("*/", i)) {
5568
+ depth--;
5569
+ i += 2;
5570
+ } else i++;
5571
+ }
5572
+ if (depth) throw new Error("unterminated SQL comment");
5573
+ } else if (sql[i] === "'" || sql[i] === '"') {
5574
+ const escapes = sql[i] === "'" && /[eE]/.test(sql[i - 1] ?? "") && !/[A-Za-z0-9_$]/.test(sql[i - 2] ?? "");
5575
+ const quote = sql[i++];
5576
+ while (i < sql.length) {
5577
+ if (sql[i] === quote) {
5578
+ if (sql[i + 1] === quote) i += 2;
5579
+ else {
5580
+ i++;
5581
+ break;
5582
+ }
5583
+ } else if (escapes && sql[i] === "\\") i += 2;
5584
+ else i++;
5585
+ }
5586
+ } else if (sql[i] === "$" && /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.test(sql.slice(i))) {
5587
+ const marker = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql.slice(i))[0];
5588
+ const end = sql.indexOf(marker, i + marker.length);
5589
+ if (end < 0) throw new Error("unterminated SQL string");
5590
+ i = end + marker.length;
5591
+ } else if (sql[i] === ":" && sql[i - 1] !== ":" && sql[i + 1] !== ":") {
5592
+ const name2 = /^[A-Za-z_][A-Za-z0-9_]*/.exec(sql.slice(i + 1))?.[0];
5593
+ if (!name2 || !names.includes(name2)) throw new Error("undeclared named SQL parameter");
5594
+ result += `$${names.indexOf(name2) + 1}`;
5595
+ i += name2.length + 1;
5596
+ continue;
5597
+ } else i++;
5598
+ result += sql.slice(start, i);
5599
+ }
5600
+ return result;
5601
+ }
5602
+
5603
+ // src/sql/operations.ts
5604
+ function isSqlOperationDocument(path, contents) {
5605
+ if (path.endsWith(".operation.json")) return true;
5606
+ if (!contents?.trimStart().startsWith("{")) return false;
5607
+ try {
5608
+ const value = JSON.parse(contents);
5609
+ return !!value && typeof value === "object" && "language" in value && value.language === "sql";
5610
+ } catch {
5611
+ return false;
5612
+ }
5613
+ }
5614
+ async function compileOperationDocuments(input) {
5615
+ const sqlDocuments = input.documents.filter((d) => isSqlOperationDocument(d.path, d.contents));
5616
+ const result = compileNamespace({ ...input, documents: input.documents.filter((d) => !isSqlOperationDocument(d.path, d.contents)) });
5617
+ if (!result.ok || !sqlDocuments.length) return result;
5618
+ for (const document of sqlDocuments) {
5619
+ try {
5620
+ const definition = defineOperation(JSON.parse(document.contents));
5621
+ if (definition.namespace !== input.canonical.namespace) throw new Error("SQL operation namespace does not match its owner");
5622
+ if (result.artifact.operations[definition.name]) throw new Error("duplicate operation name");
5623
+ const roles = input.canonical.capabilities.roles;
5624
+ const localCapabilities = roles && typeof roles === "object" && "capabilities" in roles ? roles.capabilities : void 0;
5625
+ const declaredLocally = localCapabilities && typeof localCapabilities === "object" && Object.hasOwn(localCapabilities, definition.requires.capability);
5626
+ if (!declaredLocally && !input.canonical.capabilities.uses.includes(definition.requires.capability)) throw new Error("SQL operation capability is not declared by its namespace");
5627
+ const read = [...definition.ontology.read].sort();
5628
+ const catalog = createSqlCatalog({ canonical: input.canonical, publishedObjects: read, executionTarget: "registered-operation" });
5629
+ if ("mode" in definition) {
5630
+ if (catalog.objects.some((object) => object.read.some((ref) => !read.includes(ref)))) throw new Error("SQL policy accesses an undeclared ontology object");
5631
+ const opHash2 = sqlHash(JSON.stringify([
5632
+ "sequence-runtime-sql-operation-v1",
5633
+ definition.name,
5634
+ definition.namespace,
5635
+ definition.requires.capability,
5636
+ read,
5637
+ catalog.identity,
5638
+ definition.maxRows
5639
+ ]));
5640
+ result.artifact.operations[definition.name] = compiledOperationSchema.parse({
5641
+ irVersion: RUNTIME_SQL_IR_VERSION,
5642
+ language: "sql",
5643
+ name: definition.name,
5644
+ opHash: opHash2,
5645
+ kind: "query",
5646
+ requires: definition.requires,
5647
+ variables: [
5648
+ { name: "sql", type: { kind: "scalar", scalar: "text" }, nullable: false, list: false },
5649
+ { name: "parameters", type: { kind: "scalar", scalar: "jsonb" }, nullable: true, list: false }
5650
+ ],
5651
+ roots: [],
5652
+ tableSet: read.map((ref) => ref.split("/")[1]).sort(),
5653
+ sqlQuery: { catalog, read, maxRows: definition.maxRows }
5654
+ });
5655
+ result.manifest.operations[definition.name] = { opHash: opHash2, kind: "query", requires: definition.requires };
5656
+ continue;
5657
+ }
5658
+ const names = Object.keys(definition.parameters).sort();
5659
+ if (names.length > 128) throw new Error("too many SQL parameters");
5660
+ const plan = await compileOntologySql({
5661
+ catalog,
5662
+ sql: bindNamedParameters({ sql: definition.sql, names }),
5663
+ parameterTypes: names.map((name2) => definition.parameters[name2].type),
5664
+ maxRows: definition.maxRows
5665
+ });
5666
+ if (plan.read.some((ref) => !read.includes(ref))) throw new Error("SQL policy/query accesses an undeclared ontology object");
5667
+ const variables = names.map((name2) => ({
5668
+ name: name2,
5669
+ type: { kind: "scalar", scalar: definition.parameters[name2].type },
5670
+ nullable: !definition.parameters[name2].required,
5671
+ list: false
5672
+ }));
5673
+ const opHash = sqlHash(JSON.stringify([
5674
+ "sequence-sql-operation-v1",
5675
+ definition.name,
5676
+ definition.namespace,
5677
+ definition.requires.capability,
5678
+ read,
5679
+ variables,
5680
+ plan.queryHash
5681
+ ]));
5682
+ const operation = compiledOperationSchema.parse({
5683
+ irVersion: SQL_IR_VERSION,
5684
+ language: "sql",
5685
+ name: definition.name,
5686
+ opHash,
5687
+ kind: "query",
5688
+ requires: definition.requires,
5689
+ variables,
5690
+ roots: [],
5691
+ tableSet: plan.read.map((ref) => ref.split("/")[1]).sort(),
5692
+ sql: { text: plan.text, columns: plan.columns, parameters: names, read, maxRows: plan.maxRows }
5693
+ });
5694
+ result.artifact.operations[definition.name] = operation;
5695
+ result.manifest.operations[definition.name] = { opHash, kind: "query", requires: definition.requires };
5696
+ } catch (error) {
5697
+ return { ok: false, diagnostics: [...result.diagnostics, {
5698
+ severity: "error",
5699
+ code: "invalid-sql-operation",
5700
+ document: document.path,
5701
+ message: error instanceof Error ? error.message : "invalid SQL operation"
5702
+ }] };
5703
+ }
5704
+ }
5705
+ const required = requiredArtifactIrVersion(Object.values(result.artifact.operations));
5706
+ if (required > result.artifact.irVersion) result.artifact.irVersion = required;
5707
+ result.artifact.opsHash = computeOpsHash(Object.values(result.artifact.operations).map((op) => op.opHash));
5708
+ result.manifest.opsHash = result.artifact.opsHash;
5709
+ return { ...result, artifact: normalizeIr(result.artifact) };
5710
+ }
5711
+ var parameterName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(63).refine((value) => !["__proto__", "prototype", "constructor"].includes(value), "reserved parameter name");
5712
+ var parameter = z.discriminatedUnion("type", [
5713
+ z.object({ type: z.literal("text"), value: z.string().nullable() }).strict(),
5714
+ z.object({ type: z.literal("uuid"), value: z.guid().nullable() }).strict(),
5715
+ z.object({ type: z.literal("date"), value: z.string().nullable() }).strict(),
5716
+ z.object({ type: z.literal("timestamptz"), value: z.string().nullable() }).strict(),
5717
+ z.object({ type: z.literal("numeric"), value: z.string().regex(/^-?\d+(\.\d+)?$/).nullable() }).strict(),
5718
+ z.object({ type: z.literal("bigint"), value: z.string().regex(/^-?\d+$/).nullable() }).strict(),
5719
+ z.object({ type: z.literal("integer"), value: z.number().int().min(-2147483648).max(2147483647).nullable() }).strict(),
5720
+ z.object({ type: z.literal("double"), value: z.number().nullable() }).strict(),
5721
+ z.object({ type: z.literal("boolean"), value: z.boolean().nullable() }).strict()
5722
+ ]);
5723
+ var sqlQueryInputSchema = z.object({
5724
+ sql: z.string().min(1).max(65536).refine((value) => Buffer.byteLength(value) <= 65536, "SQL exceeds 64 KiB"),
5725
+ parameters: z.record(parameterName, parameter).refine((value) => Object.keys(value).length <= 128, "too many SQL parameters").default({})
5726
+ }).strict();
5727
+ async function compileRuntimeSql({ operation, input }) {
5728
+ const contract = operation.sqlQuery;
5729
+ if (!contract) throw new Error("missing runtime SQL contract");
5730
+ const names = Object.keys(input.parameters).sort();
5731
+ const plan = await compileOntologySql({
5732
+ catalog: contract.catalog,
5733
+ sql: bindNamedParameters({ sql: input.sql, names }),
5734
+ parameterTypes: names.map((name2) => input.parameters[name2].type),
5735
+ maxRows: contract.maxRows
5736
+ });
5737
+ if (plan.read.some((ref) => !contract.read.includes(ref))) throw new Error("SQL accesses an undeclared ontology object");
5738
+ return compiledOperationSchema.parse({
5739
+ ...operation,
5740
+ sqlQuery: void 0,
5741
+ irVersion: SQL_IR_VERSION,
5742
+ variables: names.map((name2) => ({ name: name2, type: { kind: "scalar", scalar: input.parameters[name2].type }, nullable: true, list: false })),
5743
+ tableSet: plan.read.map((ref) => ref.split("/")[1]).sort(),
5744
+ sql: { text: plan.text, columns: plan.columns, parameters: names, read: plan.read, maxRows: plan.maxRows }
5745
+ });
5746
+ }
5747
+ var DATASET_PREVIEW_LIMIT = 100;
5748
+ function compileDatasetPreviews(input) {
5749
+ const canonical = normalizeCanonical(input);
5750
+ const { ctx, diagnostics } = buildCompileContext(canonical);
5751
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) throw new NamespaceCompileError(diagnostics);
5752
+ const schemaHash = computeSchemaHash(canonical);
5753
+ const operations = {};
5754
+ for (const relation of canonicalRelations(canonical)) {
5755
+ if (relation.internal || relation.source !== void 0) continue;
5756
+ const columns = relation.columns.filter((column) => column.cell === void 0);
5757
+ const { roots, tableSet } = injectPolicies({ ctx, roots: [{
5758
+ kind: "select",
5759
+ table: relation.table,
5760
+ alias: "rows",
5761
+ where: { kind: "const", value: true },
5762
+ orderBy: [],
5763
+ limit: { value: DATASET_PREVIEW_LIMIT, default: DATASET_PREVIEW_LIMIT, max: DATASET_PREVIEW_LIMIT },
5764
+ selection: {
5765
+ typename: ctx.tables[relation.table].typename,
5766
+ columns: columns.map((column) => ({ column: column.name, alias: column.name })),
5767
+ relations: [],
5768
+ aggregates: []
5769
+ }
5770
+ }] });
5771
+ for (const table of tableSet) {
5772
+ const meta = ctx.tables[table];
5773
+ if (meta.namespace && meta.namespace !== ctx.namespace && !meta.exportRead) {
5774
+ throw new DocumentCompileError(`object '${meta.namespace}/${table}' is not exported for reads`, "unexported-object");
5775
+ }
5776
+ }
5777
+ const hash = createHash("sha256").update("sequence-orm-dataset-preview-v1\0").update(stableJson({ schemaHash, roots, tableSet })).digest("hex");
5778
+ operations[relation.table] = compiledOperationSchema.parse({
5779
+ irVersion: canonical.imports?.length ? COMPOSED_IR_VERSION : LEGACY_IR_VERSION,
5780
+ name: `DatasetPreview_${relation.table}`,
5781
+ opHash: `sha256:${hash}`,
5782
+ kind: "query",
5783
+ requires: null,
5784
+ variables: [],
5785
+ tableSet,
5786
+ roots
5787
+ });
5788
+ }
5789
+ return operations;
5790
+ }
5791
+
5792
+ export { COMPILER_MAJOR, CONST_TRUE, DATASET_PREVIEW_LIMIT, DEFAULT_LIMIT, DocumentCompileError, MAX_LIMIT, NamespaceCompileError, ORDER_BY_DIRECTIONS, OperationLowerer, PLATFORM_USER_TABLE, PLATFORM_USER_TYPENAME, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, PolicyCompileError, PolicyParseError, allowedBoolExpOperators, allowedOps, andPred, assertClaimsOnlyWhen, buildCompileContext, camelCase, canonicalOperationText, classifyInputType, collectPinnedColumns, compileDatasetPreviews, compileNamespace, compileNamespaceOrThrow, compileOperationDocuments, compilePolicy, compilePredicateV2, compileRuntimeSql, computeOpHash, computeOpsHash, computePinnedColumnsByTable, computeSchemaHash, defineOperation, enumTypeName, flattenOperation, injectPolicies, inputTypeName, isSqlOperationDocument, lowerOperation, mutationRootNames, parseDocuments, parsePolicy, parseSdl, pascalCase, platformUserMeta, predContains, predContainsExists, queryRootNames, restrictOperation, sqlOperationDefinitionSchema, sqlQueryInputSchema, typeNameForTable, validateAppendInput, validateBoolExpInput, validateInsertInput, validateOnConflictInput, validateOrderByInput, validateSelectColumns, validateSetInput, walkPolicy, walkPred };
3787
5793
  //# sourceMappingURL=compiler.js.map
3788
5794
  //# sourceMappingURL=compiler.js.map