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