@sequenceholdings/orm 0.1.1 → 0.1.3

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 (81) hide show
  1. package/README.md +12 -2
  2. package/dist/authoring.cjs +1801 -0
  3. package/dist/authoring.cjs.map +1 -0
  4. package/dist/authoring.d.cts +539 -0
  5. package/dist/authoring.d.ts +539 -0
  6. package/dist/authoring.js +1759 -0
  7. package/dist/authoring.js.map +1 -0
  8. package/dist/canonical.cjs +552 -0
  9. package/dist/canonical.cjs.map +1 -0
  10. package/dist/canonical.d.cts +603 -0
  11. package/dist/canonical.d.ts +603 -0
  12. package/dist/canonical.js +532 -0
  13. package/dist/canonical.js.map +1 -0
  14. package/dist/cli.cjs +7843 -1815
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.d.cts +8 -1
  17. package/dist/cli.d.ts +8 -1
  18. package/dist/cli.js +7854 -1828
  19. package/dist/cli.js.map +1 -1
  20. package/dist/client.cjs +120 -0
  21. package/dist/client.cjs.map +1 -1
  22. package/dist/client.d.cts +74 -2
  23. package/dist/client.d.ts +74 -2
  24. package/dist/client.js +116 -1
  25. package/dist/client.js.map +1 -1
  26. package/dist/compiler.cjs +3453 -0
  27. package/dist/compiler.cjs.map +1 -0
  28. package/dist/compiler.d.cts +389 -0
  29. package/dist/compiler.d.ts +389 -0
  30. package/dist/compiler.js +3396 -0
  31. package/dist/compiler.js.map +1 -0
  32. package/dist/drizzle.cjs +14 -0
  33. package/dist/drizzle.cjs.map +1 -0
  34. package/dist/drizzle.d.cts +1 -0
  35. package/dist/drizzle.d.ts +1 -0
  36. package/dist/drizzle.js +3 -0
  37. package/dist/drizzle.js.map +1 -0
  38. package/dist/dynamic-input.cjs +391 -0
  39. package/dist/dynamic-input.cjs.map +1 -0
  40. package/dist/dynamic-input.d.cts +111 -0
  41. package/dist/dynamic-input.d.ts +111 -0
  42. package/dist/dynamic-input.js +380 -0
  43. package/dist/dynamic-input.js.map +1 -0
  44. package/dist/index.cjs +3727 -344
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +236 -59
  47. package/dist/index.d.ts +236 -59
  48. package/dist/index.js +3757 -482
  49. package/dist/index.js.map +1 -1
  50. package/dist/ir.cjs +642 -0
  51. package/dist/ir.cjs.map +1 -0
  52. package/dist/ir.d.cts +1119 -0
  53. package/dist/ir.d.ts +1119 -0
  54. package/dist/ir.js +623 -0
  55. package/dist/ir.js.map +1 -0
  56. package/dist/naming-CTvWSRTf.d.ts +79 -0
  57. package/dist/naming-DRyinU4u.d.cts +79 -0
  58. package/dist/predicate-v2.cjs +1093 -0
  59. package/dist/predicate-v2.cjs.map +1 -0
  60. package/dist/predicate-v2.d.cts +139 -0
  61. package/dist/predicate-v2.d.ts +139 -0
  62. package/dist/predicate-v2.js +1083 -0
  63. package/dist/predicate-v2.js.map +1 -0
  64. package/dist/sdl.cjs +1122 -0
  65. package/dist/sdl.cjs.map +1 -0
  66. package/dist/sdl.d.cts +94 -0
  67. package/dist/sdl.d.ts +94 -0
  68. package/dist/sdl.js +1098 -0
  69. package/dist/sdl.js.map +1 -0
  70. package/package.json +51 -3
  71. package/src/codegen-gql/apollo-hooks.cjs +98 -0
  72. package/src/codegen-gql/server-fns.cjs +101 -0
  73. package/templates/README.md +13 -0
  74. package/templates/orm-namespace/CLAUDE.md +82 -0
  75. package/templates/orm-namespace/codegen.ts +22 -0
  76. package/templates/orm-namespace/gitignore +5 -0
  77. package/templates/orm-namespace/graphql/example/queries.ts +56 -0
  78. package/templates/orm-namespace/package.json +24 -0
  79. package/templates/orm-namespace/schema/tables.ts +71 -0
  80. package/templates/orm-namespace/sequence.config.ts +32 -0
  81. package/templates/orm-namespace/tsconfig.json +15 -0
package/dist/index.js CHANGED
@@ -1,11 +1,2031 @@
1
+ import { sql, eq, notExists } from 'drizzle-orm';
2
+ import { pgSchema, uniqueIndex, index, foreignKey, primaryKey, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
3
+ import { createHash } from 'crypto';
1
4
  import { z } from 'zod';
5
+ import { createJiti } from 'jiti';
6
+ import { realpath, stat, readdir, readFile, mkdir, writeFile } from 'fs/promises';
7
+ import { createRequire } from 'module';
8
+ import { join, sep, extname } from 'path';
2
9
  import { generateDrizzleJson, upPgSnapshot, generateMigration, pushSchema } from 'drizzle-kit/api';
3
- import { pgSchema, uniqueIndex, index, foreignKey, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
4
- import { sql } from 'drizzle-orm';
5
- import { createHash } from 'crypto';
6
- import { readFile, readdir, mkdir, writeFile } from 'fs/promises';
7
- import { join, extname } from 'path';
8
10
  import { load } from 'js-yaml';
11
+ import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins } from '@sequenceholdings/custom-roles-manifest';
12
+ export { capabilityPinsSchema, customRolesBlockSchema, customRolesDeclared, parseCapabilityPin, validateCapabilityPins } from '@sequenceholdings/custom-roles-manifest';
13
+
14
+ // src/authoring/columns.ts
15
+ var COLUMN_BRAND = "seq-column";
16
+ var ENUM_BRAND = "seq-enum";
17
+ function isEnumRef(value) {
18
+ return typeof value === "object" && value !== null && value.kind === ENUM_BRAND;
19
+ }
20
+ var COLUMN_DEFAULT_FNS = ["gen_random_uuid()", "now()"];
21
+ function deriveColumnName(propertyName) {
22
+ return propertyName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase();
23
+ }
24
+ var ColumnBuilder = class {
25
+ kind = COLUMN_BRAND;
26
+ state;
27
+ constructor(type, columnName) {
28
+ this.state = {
29
+ type,
30
+ columnName,
31
+ primaryKey: false,
32
+ notNull: false,
33
+ unique: false,
34
+ hasDefault: false,
35
+ defaultValue: void 0,
36
+ defaultFn: void 0,
37
+ description: void 0,
38
+ renamedFrom: void 0,
39
+ cast: void 0,
40
+ deprecated: false
41
+ };
42
+ }
43
+ /** Marks this column as (part of) the primary key. Implies NOT NULL. */
44
+ primaryKey() {
45
+ this.state.primaryKey = true;
46
+ return this;
47
+ }
48
+ notNull() {
49
+ this.state.notNull = true;
50
+ return this;
51
+ }
52
+ unique() {
53
+ this.state.unique = true;
54
+ return this;
55
+ }
56
+ /**
57
+ * Column default: a literal, or one of the allowlisted SQL functions
58
+ * (`'gen_random_uuid()'` / `'now()'`), which serialize as `defaultFn`.
59
+ */
60
+ default(value) {
61
+ if (value === "gen_random_uuid()" || value === "now()") {
62
+ this.state.defaultFn = value;
63
+ this.state.hasDefault = false;
64
+ this.state.defaultValue = void 0;
65
+ return this;
66
+ }
67
+ this.state.hasDefault = true;
68
+ this.state.defaultValue = value;
69
+ this.state.defaultFn = void 0;
70
+ return this;
71
+ }
72
+ description(text4) {
73
+ this.state.description = text4;
74
+ return this;
75
+ }
76
+ /** Migration rename hint: the previous Postgres column name. */
77
+ renamedFrom(previousColumnName) {
78
+ this.state.renamedFrom = previousColumnName;
79
+ return this;
80
+ }
81
+ /** Migration cast hint: the `USING` SQL applied when this column changes type. */
82
+ cast(usingSql) {
83
+ this.state.cast = usingSql;
84
+ return this;
85
+ }
86
+ /** Emits `@deprecated` in the generated SDL; the column stays queryable. */
87
+ deprecated() {
88
+ this.state.deprecated = true;
89
+ return this;
90
+ }
91
+ /** Whether `.primaryKey()` was called — collected into the table-level PK. */
92
+ get isPrimaryKey() {
93
+ return this.state.primaryKey;
94
+ }
95
+ /** The enum name when this is an enum column, else undefined. */
96
+ get enumName() {
97
+ return typeof this.state.type === "object" ? this.state.type.enum : void 0;
98
+ }
99
+ /** Postgres column name: explicit override, else snake_case of the property name. */
100
+ columnNameFor(propertyName) {
101
+ return this.state.columnName ?? deriveColumnName(propertyName);
102
+ }
103
+ /** Serialize to a canonical v2 column. Validation happens in serializeNamespace. */
104
+ toCanonicalColumn(propertyName) {
105
+ const column = {
106
+ name: propertyName,
107
+ columnName: this.columnNameFor(propertyName),
108
+ type: typeof this.state.type === "object" ? { enum: this.state.type.enum } : this.state.type,
109
+ nullable: !(this.state.notNull || this.state.primaryKey),
110
+ unique: this.state.unique,
111
+ deprecated: this.state.deprecated
112
+ };
113
+ if (this.state.hasDefault) column.default = this.state.defaultValue ?? null;
114
+ if (this.state.defaultFn !== void 0) column.defaultFn = this.state.defaultFn;
115
+ if (this.state.description !== void 0) column.description = this.state.description;
116
+ if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
117
+ if (this.state.cast !== void 0) column.cast = this.state.cast;
118
+ return column;
119
+ }
120
+ };
121
+ function isColumnBuilder(value) {
122
+ return typeof value === "object" && value !== null && value.kind === COLUMN_BRAND;
123
+ }
124
+ var scalar = (type) => (columnName) => new ColumnBuilder(type, columnName);
125
+ var t = {
126
+ uuid: scalar("uuid"),
127
+ text: scalar("text"),
128
+ integer: scalar("integer"),
129
+ bigint: scalar("bigint"),
130
+ numeric: scalar("numeric"),
131
+ double: scalar("double"),
132
+ boolean: scalar("boolean"),
133
+ timestamptz: scalar("timestamptz"),
134
+ date: scalar("date"),
135
+ jsonb: scalar("jsonb"),
136
+ enum: (ref, columnName) => new ColumnBuilder({ enum: ref.name }, columnName)
137
+ };
138
+
139
+ // src/constants.ts
140
+ var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
141
+ var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
142
+ var MAX_IDENTIFIER_LENGTH = 63;
143
+ var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
144
+ var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
145
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
146
+ var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
147
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
148
+ var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
149
+ var ORM_SCHEMA_PREFIX = "orm_";
150
+ var CONSUMER_ROLE_SUFFIX = "_consumer";
151
+ var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
152
+ var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
153
+ var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
154
+ var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
155
+ var HASH_DOMAIN = "sequence-orm-definitions\0";
156
+ var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
157
+ var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
158
+ var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
159
+
160
+ // src/errors.ts
161
+ var OrmDefinitionError = class extends Error {
162
+ constructor(message, file) {
163
+ super(file ? `${file}: ${message}` : message);
164
+ this.file = file;
165
+ }
166
+ name = "OrmDefinitionError";
167
+ };
168
+ var PredicateParseError = class extends Error {
169
+ constructor(message, position) {
170
+ super(`${message} (at position ${position})`);
171
+ this.position = position;
172
+ }
173
+ name = "PredicateParseError";
174
+ };
175
+
176
+ // src/authoring/registry.ts
177
+ var CONTEXT_STACK_KEY = Symbol.for("sequence.orm.authoring.context-stack.v2");
178
+ function contextStack() {
179
+ const holder = globalThis;
180
+ const existing = holder[CONTEXT_STACK_KEY];
181
+ if (Array.isArray(existing)) {
182
+ return existing;
183
+ }
184
+ const created = [];
185
+ holder[CONTEXT_STACK_KEY] = created;
186
+ return created;
187
+ }
188
+ function beginAuthoringContext() {
189
+ const context = { currentFile: void 0, tables: [], views: [], enums: [], configs: [] };
190
+ contextStack().push(context);
191
+ return context;
192
+ }
193
+ function endAuthoringContext(context) {
194
+ const stack = contextStack();
195
+ const top = stack[stack.length - 1];
196
+ if (top !== context) {
197
+ throw new Error("endAuthoringContext: the given context is not the active one \u2014 begin/end calls are mismatched");
198
+ }
199
+ stack.pop();
200
+ }
201
+ function activeAuthoringContext() {
202
+ const stack = contextStack();
203
+ return stack[stack.length - 1];
204
+ }
205
+ function register(bucket, value) {
206
+ const context = activeAuthoringContext();
207
+ if (!context) return;
208
+ context[bucket].push({ value, file: context.currentFile });
209
+ }
210
+ function registerTable(value) {
211
+ register("tables", value);
212
+ }
213
+ function registerView(value) {
214
+ register("views", value);
215
+ }
216
+ function registerEnum(value) {
217
+ register("enums", value);
218
+ }
219
+ function registerNamespaceConfig(value) {
220
+ register("configs", value);
221
+ }
222
+
223
+ // src/authoring/table.ts
224
+ var TABLE_BRAND = "seq-table";
225
+ var LINK_BRAND = "seq-link";
226
+ function toOne(target, options) {
227
+ return {
228
+ kind: LINK_BRAND,
229
+ linkKind: "toOne",
230
+ target,
231
+ on: options.on,
232
+ references: options.references,
233
+ enforce: options.enforce ?? false,
234
+ description: options.description
235
+ };
236
+ }
237
+ function toMany(target, options) {
238
+ return {
239
+ kind: LINK_BRAND,
240
+ linkKind: "toMany",
241
+ target,
242
+ on: options.on,
243
+ references: options.references,
244
+ enforce: false,
245
+ description: options.description
246
+ };
247
+ }
248
+ function toPlatformUser(options) {
249
+ return {
250
+ kind: LINK_BRAND,
251
+ linkKind: "toPlatformUser",
252
+ target: void 0,
253
+ on: options.on,
254
+ references: options.references,
255
+ enforce: false,
256
+ description: options.description
257
+ };
258
+ }
259
+ function isSeqLink(value) {
260
+ return typeof value === "object" && value !== null && value.kind === LINK_BRAND;
261
+ }
262
+ function isSeqTable(value) {
263
+ return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
264
+ }
265
+ function assertIdentifier({ value, what, file }) {
266
+ if (!IDENTIFIER_PATTERN.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
267
+ throw new OrmDefinitionError(
268
+ `${what} "${value}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
269
+ file
270
+ );
271
+ }
272
+ }
273
+ function defineTable(name, columns, config = {}) {
274
+ const file = activeAuthoringContext()?.currentFile;
275
+ assertIdentifier({ value: name, what: "table name", file });
276
+ const entries = Object.entries(columns);
277
+ if (entries.length === 0) {
278
+ throw new OrmDefinitionError(`table "${name}" declares no columns`, file);
279
+ }
280
+ const seenColumnNames = /* @__PURE__ */ new Map();
281
+ for (const [propertyName, builder] of entries) {
282
+ if (!isColumnBuilder(builder)) {
283
+ throw new OrmDefinitionError(
284
+ `table "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
285
+ file
286
+ );
287
+ }
288
+ const columnName = builder.columnNameFor(propertyName);
289
+ assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file });
290
+ const existing = seenColumnNames.get(columnName);
291
+ if (existing !== void 0) {
292
+ throw new OrmDefinitionError(
293
+ `table "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
294
+ file
295
+ );
296
+ }
297
+ seenColumnNames.set(columnName, propertyName);
298
+ }
299
+ const table = { kind: TABLE_BRAND, tableName: name, columns, config, source: void 0, file };
300
+ registerTable(table);
301
+ return table;
302
+ }
303
+ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
304
+ const file = activeAuthoringContext()?.currentFile;
305
+ assertIdentifier({ value: name, what: "table name", file });
306
+ const isTyped = maybeConfig !== void 0;
307
+ const columns = isTyped ? columnsOrConfig : {};
308
+ const config = isTyped ? maybeConfig : columnsOrConfig;
309
+ if (!SYNCED_SOURCE_PATTERN.test(config.source)) {
310
+ throw new OrmDefinitionError(
311
+ `synced table "${name}": source "${config.source}" must match ${SYNCED_SOURCE_PATTERN} (gold layer only)`,
312
+ file
313
+ );
314
+ }
315
+ const entries = Object.entries(columns);
316
+ const seenColumnNames = /* @__PURE__ */ new Map();
317
+ const columnLevelPk = [];
318
+ for (const [propertyName, builder] of entries) {
319
+ if (!isColumnBuilder(builder)) {
320
+ throw new OrmDefinitionError(
321
+ `synced table "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
322
+ file
323
+ );
324
+ }
325
+ const columnName = builder.columnNameFor(propertyName);
326
+ assertIdentifier({
327
+ value: columnName,
328
+ what: `synced table "${name}": column "${propertyName}" column name`,
329
+ file
330
+ });
331
+ const existing = seenColumnNames.get(columnName);
332
+ if (existing !== void 0) {
333
+ throw new OrmDefinitionError(
334
+ `synced table "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
335
+ file
336
+ );
337
+ }
338
+ seenColumnNames.set(columnName, propertyName);
339
+ if (builder.isPrimaryKey) columnLevelPk.push(propertyName);
340
+ }
341
+ if (entries.length > 0) {
342
+ const hasConfigPk = config.primaryKey !== void 0 && config.primaryKey.length > 0;
343
+ if (columnLevelPk.length === 0 && !hasConfigPk) {
344
+ throw new OrmDefinitionError(
345
+ `synced table "${name}" declares columns but no primary key \u2014 use .primaryKey() or config.primaryKey`,
346
+ file
347
+ );
348
+ }
349
+ if (columnLevelPk.length > 0 && hasConfigPk) {
350
+ throw new OrmDefinitionError(
351
+ `synced table "${name}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
352
+ file
353
+ );
354
+ }
355
+ }
356
+ const tableConfig = {};
357
+ if (config.description !== void 0) tableConfig.description = config.description;
358
+ if (config.primaryKey !== void 0) tableConfig.primaryKey = config.primaryKey;
359
+ if (config.links !== void 0) tableConfig.links = config.links;
360
+ if (config.policies !== void 0) tableConfig.policies = { read: config.policies.read };
361
+ const table = {
362
+ kind: TABLE_BRAND,
363
+ tableName: name,
364
+ columns,
365
+ config: tableConfig,
366
+ source: config.source,
367
+ file
368
+ };
369
+ registerTable(table);
370
+ return table;
371
+ }
372
+ function defineEnum(name, values) {
373
+ const file = activeAuthoringContext()?.currentFile;
374
+ assertIdentifier({ value: name, what: "enum name", file });
375
+ if (values.length === 0 || values.length > 100) {
376
+ throw new OrmDefinitionError(`enum "${name}" must declare between 1 and 100 values`, file);
377
+ }
378
+ const seen = /* @__PURE__ */ new Set();
379
+ for (const value of values) {
380
+ if (value.length === 0 || value.length > MAX_IDENTIFIER_LENGTH) {
381
+ throw new OrmDefinitionError(
382
+ `enum "${name}": value ${JSON.stringify(value)} must be 1-${MAX_IDENTIFIER_LENGTH} characters`,
383
+ file
384
+ );
385
+ }
386
+ if (seen.has(value)) {
387
+ throw new OrmDefinitionError(`enum "${name}" declares duplicate value "${value}"`, file);
388
+ }
389
+ seen.add(value);
390
+ }
391
+ const ref = { kind: ENUM_BRAND, name, values: [...values] };
392
+ registerEnum(ref);
393
+ return ref;
394
+ }
395
+ var VIEW_BRAND = "seq-view";
396
+ function isSeqView(value) {
397
+ return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
398
+ }
399
+ function defineView(name, columns, config) {
400
+ const file = activeAuthoringContext()?.currentFile;
401
+ if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
402
+ throw new OrmDefinitionError(
403
+ `view name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
404
+ file
405
+ );
406
+ }
407
+ const entries = Object.entries(columns);
408
+ if (entries.length === 0) throw new OrmDefinitionError(`view "${name}" declares no columns`, file);
409
+ const seenColumnNames = /* @__PURE__ */ new Map();
410
+ for (const [propertyName, builder] of entries) {
411
+ if (!isColumnBuilder(builder)) {
412
+ throw new OrmDefinitionError(
413
+ `view "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories`,
414
+ file
415
+ );
416
+ }
417
+ const columnName = builder.columnNameFor(propertyName);
418
+ if (!IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
419
+ throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
420
+ }
421
+ const existing = seenColumnNames.get(columnName);
422
+ if (existing !== void 0) {
423
+ throw new OrmDefinitionError(
424
+ `view "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
425
+ file
426
+ );
427
+ }
428
+ seenColumnNames.set(columnName, propertyName);
429
+ }
430
+ const view = { kind: VIEW_BRAND, viewName: name, columns, config, file };
431
+ registerView(view);
432
+ return view;
433
+ }
434
+ var QUERY_COLUMN_FACTORIES = {
435
+ uuid: (name) => uuid(name),
436
+ text: (name) => text(name),
437
+ integer: (name) => integer(name),
438
+ bigint: (name) => bigint(name, { mode: "bigint" }),
439
+ numeric: (name) => numeric(name),
440
+ double: (name) => doublePrecision(name),
441
+ boolean: (name) => boolean(name),
442
+ timestamptz: (name) => timestamp(name, { withTimezone: true }),
443
+ date: (name) => date(name),
444
+ jsonb: (name) => jsonb(name)
445
+ };
446
+ function compileViewQueries(input) {
447
+ const schemaName = `orm_${input.namespace}`;
448
+ const schema = pgSchema(schemaName);
449
+ const enumFactories = /* @__PURE__ */ new Map();
450
+ for (const entry of input.enums) {
451
+ const values = entry.values;
452
+ enumFactories.set(entry.name, schema.enum(entry.name, values));
453
+ }
454
+ const handles = /* @__PURE__ */ new Map();
455
+ const physicalToLogical = /* @__PURE__ */ new Map();
456
+ const tableByName = new Map(input.tables.map((table) => [table.table, table]));
457
+ for (const authored of input.authoredTables) {
458
+ const canonical = tableByName.get(authored.tableName);
459
+ if (!canonical) continue;
460
+ const physicalSchema = schemaName;
461
+ const physicalName = canonical.table;
462
+ const relationSchema = pgSchema(physicalSchema);
463
+ const columns = buildQueryColumns(canonical.columns, enumFactories);
464
+ const handle = relationSchema.table(physicalName, columns);
465
+ handles.set(authored, handle);
466
+ physicalToLogical.set(`${physicalSchema}.${physicalName}`, canonical.table);
467
+ }
468
+ const viewByName = new Map(input.views.map((view) => [view.view, view]));
469
+ for (const authored of input.authoredViews) {
470
+ const canonical = viewByName.get(authored.viewName);
471
+ if (!canonical) continue;
472
+ const columns = buildQueryColumns(canonical.columns, enumFactories);
473
+ const handle = schema.view(canonical.view, columns).existing();
474
+ handles.set(authored, handle);
475
+ physicalToLogical.set(`${schemaName}.${canonical.view}`, canonical.view);
476
+ }
477
+ const dialect = new PgDialect();
478
+ const compiled = /* @__PURE__ */ new Map();
479
+ for (const authored of input.authoredViews) {
480
+ const expectedFields = Object.keys(authored.columns);
481
+ const declaredDependencies = /* @__PURE__ */ new Set();
482
+ const relation = (ref) => {
483
+ let resolved;
484
+ try {
485
+ resolved = typeof ref === "function" ? ref() : ref;
486
+ } catch (error) {
487
+ throw new OrmDefinitionError(
488
+ `view "${authored.viewName}" relation thunk threw: ${error instanceof Error ? error.message : String(error)}`,
489
+ authored.file
490
+ );
491
+ }
492
+ if (!isSeqTable(resolved) && !isSeqView(resolved)) {
493
+ throw new OrmDefinitionError(
494
+ `view "${authored.viewName}" relation() expects a table or view defined by this ORM`,
495
+ authored.file
496
+ );
497
+ }
498
+ const handle = handles.get(resolved);
499
+ if (!handle) {
500
+ const name = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
501
+ throw new OrmDefinitionError(
502
+ `view "${authored.viewName}" references relation "${name}" which is not part of this namespace`,
503
+ authored.file
504
+ );
505
+ }
506
+ declaredDependencies.add(isSeqTable(resolved) ? resolved.tableName : resolved.viewName);
507
+ return handle;
508
+ };
509
+ let queryBuilder;
510
+ try {
511
+ queryBuilder = authored.config.query({ qb: new QueryBuilder(), relation });
512
+ } catch (error) {
513
+ if (error instanceof OrmDefinitionError) throw error;
514
+ throw new OrmDefinitionError(
515
+ `view "${authored.viewName}" query callback failed: ${error instanceof Error ? error.message : String(error)}`,
516
+ authored.file
517
+ );
518
+ }
519
+ let config;
520
+ try {
521
+ config = getViewConfig(schema.view(authored.viewName).as(queryBuilder));
522
+ } catch (error) {
523
+ throw new OrmDefinitionError(
524
+ `view "${authored.viewName}" query must return a Drizzle select builder: ${error instanceof Error ? error.message : String(error)}`,
525
+ authored.file
526
+ );
527
+ }
528
+ const selectedFields = Object.keys(config.selectedFields);
529
+ if (selectedFields.length !== expectedFields.length || selectedFields.some((field, index3) => field !== expectedFields[index3])) {
530
+ throw new OrmDefinitionError(
531
+ `view "${authored.viewName}" query selects [${selectedFields.join(", ")}], but declared columns are [${expectedFields.join(", ")}]`,
532
+ authored.file
533
+ );
534
+ }
535
+ if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
536
+ const tableDependencies = extractUsedTable(config.query).map((physical) => {
537
+ const logical = physicalToLogical.get(physical);
538
+ if (!logical) {
539
+ throw new OrmDefinitionError(
540
+ `view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
541
+ authored.file
542
+ );
543
+ }
544
+ return logical;
545
+ });
546
+ const renderedQuery = dialect.sqlToQuery(config.query.inlineParams());
547
+ if (renderedQuery.params.length > 0) {
548
+ throw new OrmDefinitionError(
549
+ `view "${authored.viewName}" query produced bind parameters which cannot be persisted in view DDL`,
550
+ authored.file
551
+ );
552
+ }
553
+ compiled.set(authored.viewName, {
554
+ query: renderedQuery.sql,
555
+ dependencies: [.../* @__PURE__ */ new Set([...declaredDependencies, ...tableDependencies])].sort()
556
+ });
557
+ }
558
+ return compiled;
559
+ }
560
+ function buildQueryColumns(columns, enumFactories) {
561
+ return Object.fromEntries(columns.map((column) => [column.name, buildQueryColumn(column, enumFactories)]));
562
+ }
563
+ function buildQueryColumn(column, enumFactories) {
564
+ const factory = typeof column.type === "string" ? QUERY_COLUMN_FACTORIES[column.type] : enumFactories.get(column.type.enum);
565
+ if (!factory) {
566
+ const type = typeof column.type === "string" ? column.type : column.type.enum;
567
+ throw new OrmDefinitionError(`view query column '${column.columnName}' references unknown type '${type}'`);
568
+ }
569
+ let builder = factory(column.columnName);
570
+ if (!column.nullable) builder = builder.notNull();
571
+ return builder;
572
+ }
573
+ function relationName(relation) {
574
+ return isSeqTable(relation) ? relation.tableName : relation.viewName;
575
+ }
576
+ function relationKind(relation) {
577
+ return isSeqTable(relation) ? "table" : "view";
578
+ }
579
+ function declaredPrimaryKeyProperties(table) {
580
+ const fromColumns = Object.entries(table.columns).filter(([, builder]) => builder.isPrimaryKey).map(([propertyName]) => propertyName);
581
+ if (fromColumns.length > 0) return fromColumns;
582
+ const configPk = table.config.primaryKey ?? [];
583
+ const resolved = [];
584
+ for (const ref of configPk) {
585
+ if (ref in table.columns) {
586
+ resolved.push(ref);
587
+ continue;
588
+ }
589
+ const byColumnName = Object.entries(table.columns).find(
590
+ ([propertyName, builder]) => builder.columnNameFor(propertyName) === ref
591
+ );
592
+ if (byColumnName) resolved.push(byColumnName[0]);
593
+ }
594
+ return resolved;
595
+ }
596
+ function assertOverlayWinsPrimaryKey({
597
+ table,
598
+ pk,
599
+ role
600
+ }) {
601
+ if (!(pk in table.columns)) {
602
+ throw new OrmDefinitionError(
603
+ `overlayWinsUnion: pk "${pk}" must exist as a property on both overlay and synced tables`
604
+ );
605
+ }
606
+ const declared = declaredPrimaryKeyProperties(table);
607
+ if (declared.length !== 1 || declared[0] !== pk) {
608
+ throw new OrmDefinitionError(
609
+ `overlayWinsUnion: ${role} primary key must be exactly "${pk}" (declared: ${declared.length > 0 ? declared.join(", ") : "none"})`
610
+ );
611
+ }
612
+ }
613
+ function formatCanonicalColumnType(type) {
614
+ return typeof type === "string" ? type : `enum(${type.enum})`;
615
+ }
616
+ function assertOverlayWinsMatchingColumnTypes({
617
+ overlay,
618
+ synced,
619
+ columns
620
+ }) {
621
+ for (const column of columns) {
622
+ const overlayType = overlay.columns[column].toCanonicalColumn(column).type;
623
+ const syncedType = synced.columns[column].toCanonicalColumn(column).type;
624
+ if (JSON.stringify(overlayType) !== JSON.stringify(syncedType)) {
625
+ throw new OrmDefinitionError(
626
+ `overlayWinsUnion: column "${column}" must have the same type on overlay and synced (overlay: ${formatCanonicalColumnType(overlayType)}, synced: ${formatCanonicalColumnType(syncedType)})`
627
+ );
628
+ }
629
+ }
630
+ }
631
+ function overlayWinsUnion(input) {
632
+ const { overlay, synced, pk, tombstone } = input;
633
+ return ({ qb, relation }) => {
634
+ assertOverlayWinsPrimaryKey({ table: overlay, pk, role: "overlay" });
635
+ assertOverlayWinsPrimaryKey({ table: synced, pk, role: "synced" });
636
+ const overlayHandle = relation(overlay);
637
+ const syncedHandle = relation(synced);
638
+ const overlayCols = overlayHandle;
639
+ const syncedCols = syncedHandle;
640
+ const overlayPk = overlayCols[pk];
641
+ const syncedPk = syncedCols[pk];
642
+ if (tombstone !== void 0) {
643
+ const tombstoneBuilder = overlay.columns[tombstone];
644
+ if (!tombstoneBuilder) {
645
+ throw new OrmDefinitionError(
646
+ `overlayWinsUnion: tombstone "${tombstone}" must exist as a property on the overlay table`
647
+ );
648
+ }
649
+ const tombstoneCanonical = tombstoneBuilder.toCanonicalColumn(tombstone);
650
+ if (tombstoneCanonical.type !== "boolean") {
651
+ throw new OrmDefinitionError(
652
+ `overlayWinsUnion: tombstone "${tombstone}" must be a boolean column`
653
+ );
654
+ }
655
+ if (tombstoneCanonical.nullable) {
656
+ throw new OrmDefinitionError(
657
+ `overlayWinsUnion: tombstone "${tombstone}" must be notNull \u2014 nullable tombstones silently drop rows`
658
+ );
659
+ }
660
+ }
661
+ const overlayColumns = Object.keys(overlay.columns);
662
+ const syncedColumns = Object.keys(synced.columns);
663
+ const sharedColumns = overlayColumns.filter((col) => syncedColumns.includes(col) && col !== pk);
664
+ assertOverlayWinsMatchingColumnTypes({
665
+ overlay,
666
+ synced,
667
+ columns: [pk, ...sharedColumns]
668
+ });
669
+ const overlaySelect = {
670
+ [pk]: overlayPk,
671
+ ...Object.fromEntries(sharedColumns.map((col) => [col, overlayCols[col]])),
672
+ provenance: sql`'overlay'`.as("provenance")
673
+ };
674
+ const syncedSelect = {
675
+ [pk]: syncedPk,
676
+ ...Object.fromEntries(sharedColumns.map((col) => [col, syncedCols[col]])),
677
+ provenance: sql`'synced'`.as("provenance")
678
+ };
679
+ const overlayFrom = qb.select(overlaySelect).from(overlayHandle);
680
+ const overlayQuery = tombstone !== void 0 ? overlayFrom.where(eq(overlayCols[tombstone], false)) : overlayFrom;
681
+ const syncedQuery = qb.select(syncedSelect).from(syncedHandle).where(
682
+ notExists(
683
+ qb.select({ one: sql`1` }).from(overlayHandle).where(eq(overlayPk, syncedPk))
684
+ )
685
+ );
686
+ return overlayQuery.unionAll(syncedQuery);
687
+ };
688
+ }
689
+
690
+ // src/authoring/cow-mutations.ts
691
+ function overrideRowMutation(input) {
692
+ const { overlay, synced, pk } = input;
693
+ const name = input.name ?? `override_${overlay.tableName}`;
694
+ assertMutationName(name, "overrideRowMutation");
695
+ assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "overrideRowMutation" });
696
+ const sharedProperties = sharedNonPkProperties({ overlay, synced, pk });
697
+ const setProperties = resolveSetProperties({
698
+ overlay,
699
+ synced,
700
+ pk,
701
+ set: input.set,
702
+ sharedProperties,
703
+ helper: "overrideRowMutation"
704
+ });
705
+ assertMatchingColumns({
706
+ overlay,
707
+ synced,
708
+ properties: [pk, ...sharedProperties],
709
+ helper: "overrideRowMutation"
710
+ });
711
+ return {
712
+ kind: "override",
713
+ name,
714
+ synced: synced.tableName,
715
+ pk,
716
+ copy: [pk, ...sharedProperties],
717
+ set: setProperties
718
+ };
719
+ }
720
+ function revertOverlayMutation(input) {
721
+ const { overlay, synced, pk } = input;
722
+ const name = input.name ?? `revert_${overlay.tableName}`;
723
+ assertMutationName(name, "revertOverlayMutation");
724
+ assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "revertOverlayMutation" });
725
+ assertMatchingColumns({ overlay, synced, properties: [pk], helper: "revertOverlayMutation" });
726
+ return {
727
+ kind: "revert",
728
+ name,
729
+ synced: synced.tableName,
730
+ pk,
731
+ copy: [],
732
+ set: []
733
+ };
734
+ }
735
+ function assertMutationName(name, helper) {
736
+ if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
737
+ throw new OrmDefinitionError(
738
+ `${helper}: mutation name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`
739
+ );
740
+ }
741
+ }
742
+ function assertOverlayPairPrimaryKey({
743
+ overlay,
744
+ synced,
745
+ pk,
746
+ helper
747
+ }) {
748
+ assertSinglePrimaryKey({ table: overlay, pk, role: "overlay", helper });
749
+ assertSinglePrimaryKey({ table: synced, pk, role: "synced", helper });
750
+ }
751
+ function assertSinglePrimaryKey({
752
+ table,
753
+ pk,
754
+ role,
755
+ helper
756
+ }) {
757
+ if (!(pk in table.columns)) {
758
+ throw new OrmDefinitionError(`${helper}: pk "${pk}" must exist as a property on the ${role} table`);
759
+ }
760
+ const declared = declaredPrimaryKeyProperties2(table);
761
+ if (declared.length !== 1 || declared[0] !== pk) {
762
+ throw new OrmDefinitionError(
763
+ `${helper}: ${role} primary key must be exactly "${pk}" (declared: ${declared.length > 0 ? declared.join(", ") : "none"})`
764
+ );
765
+ }
766
+ }
767
+ function declaredPrimaryKeyProperties2(table) {
768
+ const fromColumns = Object.entries(table.columns).filter(([, builder]) => builder.isPrimaryKey).map(([propertyName]) => propertyName);
769
+ if (fromColumns.length > 0) return fromColumns;
770
+ const configPk = table.config?.primaryKey ?? [];
771
+ const resolved = [];
772
+ for (const ref of configPk) {
773
+ if (ref in table.columns) {
774
+ resolved.push(ref);
775
+ continue;
776
+ }
777
+ const byColumnName = Object.entries(table.columns).find(
778
+ ([propertyName, builder]) => builder.columnNameFor(propertyName) === ref
779
+ );
780
+ if (byColumnName) resolved.push(byColumnName[0]);
781
+ }
782
+ return resolved;
783
+ }
784
+ function sharedNonPkProperties({
785
+ overlay,
786
+ synced,
787
+ pk
788
+ }) {
789
+ return Object.keys(overlay.columns).filter((property) => property !== pk && property in synced.columns);
790
+ }
791
+ function resolveSetProperties({
792
+ overlay,
793
+ synced,
794
+ pk,
795
+ set,
796
+ sharedProperties,
797
+ helper
798
+ }) {
799
+ const properties = set !== void 0 ? [...set] : [...sharedProperties];
800
+ if (properties.length === 0) {
801
+ throw new OrmDefinitionError(
802
+ `${helper}: set must declare at least one non-PK column to update after the copy`
803
+ );
804
+ }
805
+ const seen = /* @__PURE__ */ new Set();
806
+ for (const property of properties) {
807
+ if (property === pk) {
808
+ throw new OrmDefinitionError(`${helper}: set must not include pk "${pk}" \u2014 identity columns are pinned`);
809
+ }
810
+ if (!(property in overlay.columns)) {
811
+ throw new OrmDefinitionError(`${helper}: set column "${property}" must exist on the overlay table`);
812
+ }
813
+ if (!(property in synced.columns)) {
814
+ throw new OrmDefinitionError(
815
+ `${helper}: set column "${property}" must exist on the synced table (contract columns only)`
816
+ );
817
+ }
818
+ if (seen.has(property)) {
819
+ throw new OrmDefinitionError(`${helper}: set column "${property}" is duplicated`);
820
+ }
821
+ seen.add(property);
822
+ }
823
+ return properties;
824
+ }
825
+ function assertMatchingColumns({
826
+ overlay,
827
+ synced,
828
+ properties,
829
+ helper
830
+ }) {
831
+ for (const property of properties) {
832
+ const overlayCanonical = overlay.columns[property].toCanonicalColumn(property);
833
+ const syncedCanonical = synced.columns[property].toCanonicalColumn(property);
834
+ const overlayType = scalarColumnType(overlay, property, helper);
835
+ const syncedType = scalarColumnType(synced, property, helper);
836
+ if (overlayType !== syncedType) {
837
+ throw new OrmDefinitionError(
838
+ `${helper}: column "${property}" must have the same scalar type on overlay and synced (overlay: ${overlayType}, synced: ${syncedType})`
839
+ );
840
+ }
841
+ const overlayColumn = overlay.columns[property].columnNameFor(property);
842
+ const syncedColumn = synced.columns[property].columnNameFor(property);
843
+ if (overlayColumn !== syncedColumn) {
844
+ throw new OrmDefinitionError(
845
+ `${helper}: column "${property}" maps to different Postgres names on overlay (${overlayColumn}) and synced (${syncedColumn})`
846
+ );
847
+ }
848
+ if (syncedCanonical.nullable && !overlayCanonical.nullable) {
849
+ throw new OrmDefinitionError(
850
+ `${helper}: column "${property}" is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy-from-synced insert; make overlay nullable or synced notNull`
851
+ );
852
+ }
853
+ }
854
+ }
855
+ function scalarColumnType(table, property, helper) {
856
+ const type = table.columns[property].toCanonicalColumn(property).type;
857
+ if (typeof type !== "string") {
858
+ throw new OrmDefinitionError(
859
+ `${helper}: column "${property}" on "${table.tableName}" must be a scalar type (enums are not supported in cow mutation arguments)`
860
+ );
861
+ }
862
+ return type;
863
+ }
864
+ var CANONICAL_FORMAT = 2;
865
+ var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
866
+ var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
867
+ var COLUMN_TYPES = [
868
+ "uuid",
869
+ "text",
870
+ "integer",
871
+ "bigint",
872
+ "numeric",
873
+ "double",
874
+ "boolean",
875
+ "timestamptz",
876
+ "date",
877
+ "jsonb"
878
+ ];
879
+ var columnTypeSchema = z.enum(COLUMN_TYPES);
880
+ var canonicalEnumSchema = z.object({
881
+ name: identifier,
882
+ values: z.array(z.string().min(1).max(63)).min(1).max(100)
883
+ });
884
+ var canonicalColumnSchema = z.object({
885
+ /** Property name in TS (camelCase); columnName is the Postgres name. */
886
+ name: z.string().min(1).max(63),
887
+ columnName: identifier,
888
+ type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
889
+ nullable: z.boolean().default(true),
890
+ unique: z.boolean().default(false),
891
+ /** Literal default, or one of the allowlisted SQL functions. */
892
+ default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
893
+ defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
894
+ description: z.string().max(500).optional(),
895
+ /** Migration hints — same semantics as v1. */
896
+ renamedFrom: identifier.optional(),
897
+ cast: z.string().max(200).optional(),
898
+ deprecated: z.boolean().default(false)
899
+ });
900
+ var canonicalLinkSchema = z.object({
901
+ name: z.string().min(1).max(63),
902
+ kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
903
+ /** Target table name (namespace-local). Absent for toPlatformUser. */
904
+ target: identifier.optional(),
905
+ /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
906
+ on: identifier,
907
+ /** Referenced column on the target (default: target's PK / platform user `sub`). */
908
+ references: identifier.optional(),
909
+ /** enforce: true emits a real FK constraint (same-namespace toOne only). */
910
+ enforce: z.boolean().default(false),
911
+ description: z.string().max(500).optional()
912
+ });
913
+ var canonicalIndexSchema = z.object({
914
+ columns: z.array(identifier).min(1),
915
+ unique: z.boolean().default(false),
916
+ name: identifier.optional()
917
+ });
918
+ var policyVariantSchema = z.object({
919
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
920
+ when: z.string().min(1).max(1e3),
921
+ /** Exactly one of: allow-all bypass, or a row predicate. */
922
+ allow: z.literal(true).optional(),
923
+ policy: z.string().min(1).max(2e3).optional()
924
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
925
+ message: "A variant declares exactly one of allow: true or policy"
926
+ });
927
+ var policyExpressionSchema = z.union([
928
+ z.string().min(1).max(2e3),
929
+ z.object({
930
+ default: z.string().min(1).max(2e3),
931
+ variants: z.array(policyVariantSchema).max(10).default([])
932
+ })
933
+ ]);
934
+ var canonicalPoliciesSchema = z.object({
935
+ read: policyExpressionSchema.optional(),
936
+ write: policyExpressionSchema.optional(),
937
+ insert: policyExpressionSchema.optional(),
938
+ update: policyExpressionSchema.optional()
939
+ });
940
+ function isSingleViewQuerySql(value) {
941
+ const query = value.trim();
942
+ if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
943
+ let state = "normal";
944
+ for (let index3 = 0; index3 < query.length; index3 += 1) {
945
+ const char = query[index3];
946
+ const next = query[index3 + 1];
947
+ if (state === "single") {
948
+ if (char === "'" && next === "'") index3 += 1;
949
+ else if (char === "'") state = "normal";
950
+ continue;
951
+ }
952
+ if (state === "double") {
953
+ if (char === '"' && next === '"') index3 += 1;
954
+ else if (char === '"') state = "normal";
955
+ continue;
956
+ }
957
+ if (char === "'") state = "single";
958
+ else if (char === '"') state = "double";
959
+ else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
960
+ }
961
+ return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
962
+ }
963
+ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
964
+ "abs",
965
+ "age",
966
+ "array_agg",
967
+ "array_append",
968
+ "array_cat",
969
+ "array_dims",
970
+ "array_length",
971
+ "array_lower",
972
+ "array_ndims",
973
+ "array_position",
974
+ "array_positions",
975
+ "array_prepend",
976
+ "array_remove",
977
+ "array_replace",
978
+ "array_to_string",
979
+ "array_trim",
980
+ "array_upper",
981
+ "ascii",
982
+ "avg",
983
+ "bit_and",
984
+ "bit_or",
985
+ "bit_xor",
986
+ "bool_and",
987
+ "bool_or",
988
+ "btrim",
989
+ "cardinality",
990
+ "cbrt",
991
+ "ceil",
992
+ "ceiling",
993
+ "char_length",
994
+ "character_length",
995
+ "chr",
996
+ "concat",
997
+ "concat_ws",
998
+ "count",
999
+ "cume_dist",
1000
+ "date_bin",
1001
+ "date_part",
1002
+ "date_trunc",
1003
+ "decode",
1004
+ "degrees",
1005
+ "dense_rank",
1006
+ "encode",
1007
+ "enum_first",
1008
+ "enum_last",
1009
+ "enum_range",
1010
+ "every",
1011
+ "exp",
1012
+ "first_value",
1013
+ "floor",
1014
+ "format",
1015
+ "grouping",
1016
+ "initcap",
1017
+ "json_agg",
1018
+ "json_array_length",
1019
+ "json_build_array",
1020
+ "json_build_object",
1021
+ "json_extract_path",
1022
+ "json_extract_path_text",
1023
+ "json_object_agg",
1024
+ "json_object_keys",
1025
+ "json_strip_nulls",
1026
+ "json_typeof",
1027
+ "jsonb_agg",
1028
+ "jsonb_array_length",
1029
+ "jsonb_build_array",
1030
+ "jsonb_build_object",
1031
+ "jsonb_extract_path",
1032
+ "jsonb_extract_path_text",
1033
+ "jsonb_insert",
1034
+ "jsonb_object_agg",
1035
+ "jsonb_object_keys",
1036
+ "jsonb_path_exists",
1037
+ "jsonb_path_match",
1038
+ "jsonb_path_query",
1039
+ "jsonb_path_query_array",
1040
+ "jsonb_path_query_first",
1041
+ "jsonb_pretty",
1042
+ "jsonb_set",
1043
+ "jsonb_set_lax",
1044
+ "jsonb_strip_nulls",
1045
+ "jsonb_typeof",
1046
+ "justify_days",
1047
+ "justify_hours",
1048
+ "justify_interval",
1049
+ "lag",
1050
+ "last_value",
1051
+ "lead",
1052
+ "left",
1053
+ "length",
1054
+ "ln",
1055
+ "log",
1056
+ "lower",
1057
+ "lpad",
1058
+ "ltrim",
1059
+ "make_date",
1060
+ "make_interval",
1061
+ "make_time",
1062
+ "make_timestamp",
1063
+ "make_timestamptz",
1064
+ "max",
1065
+ "md5",
1066
+ "min",
1067
+ "mod",
1068
+ "nth_value",
1069
+ "ntile",
1070
+ "octet_length",
1071
+ "percent_rank",
1072
+ "pi",
1073
+ "power",
1074
+ "quote_ident",
1075
+ "quote_literal",
1076
+ "quote_nullable",
1077
+ "radians",
1078
+ "rank",
1079
+ "regexp_count",
1080
+ "regexp_instr",
1081
+ "regexp_like",
1082
+ "regexp_match",
1083
+ "regexp_matches",
1084
+ "regexp_replace",
1085
+ "regexp_split_to_array",
1086
+ "regexp_split_to_table",
1087
+ "repeat",
1088
+ "replace",
1089
+ "reverse",
1090
+ "right",
1091
+ "round",
1092
+ "row_number",
1093
+ "rpad",
1094
+ "rtrim",
1095
+ "sign",
1096
+ "split_part",
1097
+ "sqrt",
1098
+ "starts_with",
1099
+ "string_agg",
1100
+ "string_to_array",
1101
+ "strpos",
1102
+ "substr",
1103
+ "substring",
1104
+ "sum",
1105
+ "timezone",
1106
+ "to_ascii",
1107
+ "to_char",
1108
+ "to_date",
1109
+ "to_hex",
1110
+ "to_json",
1111
+ "to_jsonb",
1112
+ "to_number",
1113
+ "to_timestamp",
1114
+ "translate",
1115
+ "trunc",
1116
+ "unnest",
1117
+ "upper",
1118
+ "width_bucket"
1119
+ ];
1120
+ var canonicalActionSchema = z.object({
1121
+ name: identifier,
1122
+ requires: z.string().max(1e3).optional(),
1123
+ params: z.record(z.string(), z.object({
1124
+ type: columnTypeSchema,
1125
+ nullable: z.boolean().default(false)
1126
+ })).default({}),
1127
+ executes: z.string().min(1).max(1e4),
1128
+ returns: z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
1129
+ audit: z.object({ action: z.string(), resource: z.string() }).optional()
1130
+ });
1131
+ var canonicalCowMutationSchema = z.object({
1132
+ kind: z.enum(["override", "revert"]),
1133
+ name: identifier,
1134
+ /** Synced table (same namespace) the overlay copies from / falls back to. */
1135
+ synced: identifier,
1136
+ /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
1137
+ pk: z.string().min(1).max(63),
1138
+ /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
1139
+ copy: z.array(z.string().min(1).max(63)).default([]),
1140
+ /** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
1141
+ set: z.array(z.string().min(1).max(63)).default([])
1142
+ }).superRefine((mutation, ctx) => {
1143
+ if (mutation.kind === "override") {
1144
+ if (mutation.set.length === 0) {
1145
+ ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
1146
+ }
1147
+ if (!mutation.copy.includes(mutation.pk)) {
1148
+ ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
1149
+ }
1150
+ } else if (mutation.set.length > 0 || mutation.copy.length > 0) {
1151
+ ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
1152
+ }
1153
+ if (mutation.set.includes(mutation.pk)) {
1154
+ ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
1155
+ }
1156
+ for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
1157
+ if (new Set(values).size !== values.length) {
1158
+ ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
1159
+ }
1160
+ }
1161
+ });
1162
+ var canonicalTableSchema = z.object({
1163
+ table: identifier,
1164
+ description: z.string().max(2e3).optional(),
1165
+ /** Table-level rename hint (new in v2). */
1166
+ renamedFrom: identifier.optional(),
1167
+ /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
1168
+ source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
1169
+ columns: z.array(canonicalColumnSchema).default([]),
1170
+ primaryKey: z.array(identifier).min(1).max(8).optional(),
1171
+ links: z.array(canonicalLinkSchema).default([]),
1172
+ indexes: z.array(canonicalIndexSchema).default([]),
1173
+ policies: canonicalPoliciesSchema.default({}),
1174
+ /**
1175
+ * v1-conversion carry-over only. v2 has no action surface — the runtime
1176
+ * neither compiles nor executes these; writes are mutations. Kept parseable
1177
+ * so stored definitions and their content hashes stay stable.
1178
+ */
1179
+ actions: z.array(canonicalActionSchema).default([]),
1180
+ /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
1181
+ cowMutations: z.array(canonicalCowMutationSchema).optional()
1182
+ });
1183
+ var canonicalViewSchema = z.object({
1184
+ view: identifier,
1185
+ description: z.string().max(2e3).optional(),
1186
+ columns: z.array(canonicalColumnSchema).min(1),
1187
+ /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
1188
+ primaryKey: z.array(identifier).min(1).max(8).optional(),
1189
+ links: z.array(canonicalLinkSchema).default([]),
1190
+ policies: z.object({ read: policyExpressionSchema }).strict(),
1191
+ query: z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
1192
+ message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
1193
+ }),
1194
+ /** Namespace-local logical relation names, sorted for deterministic hashing. */
1195
+ dependencies: z.array(identifier).max(500)
1196
+ }).superRefine((view, ctx) => {
1197
+ for (const [index3, column] of view.columns.entries()) {
1198
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
1199
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
1200
+ }
1201
+ if (column.unique) {
1202
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
1203
+ }
1204
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
1205
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
1206
+ }
1207
+ }
1208
+ for (const [index3, link] of view.links.entries()) {
1209
+ if (link.enforce) {
1210
+ ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
1211
+ }
1212
+ }
1213
+ });
1214
+ var canonicalDefinitionSchema = z.object({
1215
+ definition_format: z.literal(CANONICAL_FORMAT),
1216
+ namespace: identifier.max(41),
1217
+ description: z.string().max(2e3).optional(),
1218
+ enums: z.array(canonicalEnumSchema).default([]),
1219
+ tables: z.array(canonicalTableSchema).min(0).max(500),
1220
+ /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
1221
+ views: z.array(canonicalViewSchema).max(500).optional(),
1222
+ /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
1223
+ capabilities: z.object({
1224
+ roles: z.unknown().optional(),
1225
+ uses: z.array(z.string()).default([])
1226
+ }).default({ uses: [] })
1227
+ }).superRefine((definition, ctx) => {
1228
+ const seen = /* @__PURE__ */ new Set();
1229
+ for (const [index3, table] of definition.tables.entries()) {
1230
+ if (seen.has(table.table)) {
1231
+ ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
1232
+ }
1233
+ seen.add(table.table);
1234
+ }
1235
+ const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
1236
+ for (const [tableIndex, table] of definition.tables.entries()) {
1237
+ const cowMutations = table.cowMutations ?? [];
1238
+ if (cowMutations.length === 0) continue;
1239
+ const path = (index3, ...rest) => ["tables", tableIndex, "cowMutations", index3, ...rest];
1240
+ if (table.source !== void 0) {
1241
+ ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
1242
+ continue;
1243
+ }
1244
+ for (const [index3, mutation] of cowMutations.entries()) {
1245
+ const synced = tableByName.get(mutation.synced);
1246
+ if (!synced || synced.source === void 0) {
1247
+ ctx.addIssue({
1248
+ code: "custom",
1249
+ path: path(index3, "synced"),
1250
+ message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
1251
+ });
1252
+ continue;
1253
+ }
1254
+ const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
1255
+ const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
1256
+ let columnsExist = true;
1257
+ for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
1258
+ if (!overlayColumnsByName.has(property)) {
1259
+ columnsExist = false;
1260
+ ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
1261
+ }
1262
+ }
1263
+ for (const property of [mutation.pk, ...mutation.copy]) {
1264
+ if (!syncedColumnsByName.has(property)) {
1265
+ columnsExist = false;
1266
+ ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
1267
+ }
1268
+ }
1269
+ if (!columnsExist) continue;
1270
+ const overlayPk = table.primaryKey ?? [];
1271
+ const syncedPk = synced.primaryKey ?? [];
1272
+ if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
1273
+ ctx.addIssue({
1274
+ code: "custom",
1275
+ path: path(index3, "pk"),
1276
+ message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
1277
+ });
1278
+ }
1279
+ for (const property of [mutation.pk, ...mutation.copy]) {
1280
+ const overlayColumn = overlayColumnsByName.get(property);
1281
+ const syncedColumn = syncedColumnsByName.get(property);
1282
+ if (!overlayColumn || !syncedColumn) continue;
1283
+ if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
1284
+ ctx.addIssue({
1285
+ code: "custom",
1286
+ path: path(index3),
1287
+ message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
1288
+ });
1289
+ }
1290
+ if (overlayColumn.columnName !== syncedColumn.columnName) {
1291
+ ctx.addIssue({
1292
+ code: "custom",
1293
+ path: path(index3),
1294
+ message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
1295
+ });
1296
+ }
1297
+ if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
1298
+ ctx.addIssue({
1299
+ code: "custom",
1300
+ path: path(index3),
1301
+ message: `cow mutation '${mutation.name}': column '${property}' is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy`
1302
+ });
1303
+ }
1304
+ }
1305
+ }
1306
+ }
1307
+ for (const [index3, view] of (definition.views ?? []).entries()) {
1308
+ if (seen.has(view.view)) {
1309
+ ctx.addIssue({ code: "custom", path: ["views", index3, "view"], message: `duplicate relation '${view.view}'` });
1310
+ }
1311
+ seen.add(view.view);
1312
+ }
1313
+ const views = definition.views ?? [];
1314
+ const viewByName = new Map(views.map((view) => [view.view, view]));
1315
+ for (const [viewIndex, view] of views.entries()) {
1316
+ for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
1317
+ if (!seen.has(dependency)) {
1318
+ ctx.addIssue({
1319
+ code: "custom",
1320
+ path: ["views", viewIndex, "dependencies", dependencyIndex],
1321
+ message: `view '${view.view}' depends on unknown relation '${dependency}'`
1322
+ });
1323
+ }
1324
+ }
1325
+ }
1326
+ const visiting = /* @__PURE__ */ new Set();
1327
+ const visited = /* @__PURE__ */ new Set();
1328
+ let cycleReported = false;
1329
+ const visit = (name, path) => {
1330
+ if (visited.has(name) || cycleReported) return;
1331
+ if (visiting.has(name)) {
1332
+ cycleReported = true;
1333
+ ctx.addIssue({
1334
+ code: "custom",
1335
+ path: ["views"],
1336
+ message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
1337
+ });
1338
+ return;
1339
+ }
1340
+ visiting.add(name);
1341
+ const view = viewByName.get(name);
1342
+ for (const dependency of view?.dependencies ?? []) {
1343
+ if (viewByName.has(dependency)) visit(dependency, [...path, name]);
1344
+ }
1345
+ visiting.delete(name);
1346
+ visited.add(name);
1347
+ };
1348
+ for (const view of views) visit(view.view, []);
1349
+ });
1350
+ function canonicalRelations(definition) {
1351
+ const views = (definition.views ?? []).map((view) => ({
1352
+ table: view.view,
1353
+ ...view.description !== void 0 ? { description: view.description } : {},
1354
+ columns: view.columns,
1355
+ ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
1356
+ links: view.links,
1357
+ indexes: [],
1358
+ policies: { read: view.policies.read },
1359
+ actions: []
1360
+ }));
1361
+ return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
1362
+ }
1363
+ function normalizeCanonical(definition) {
1364
+ const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
1365
+ const normalized = {
1366
+ ...definition,
1367
+ enums: sortByName(definition.enums),
1368
+ tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
1369
+ const { cowMutations, ...rest } = t2;
1370
+ return {
1371
+ ...rest,
1372
+ links: sortByName(t2.links),
1373
+ indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
1374
+ actions: sortByName(t2.actions),
1375
+ // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
1376
+ ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
1377
+ };
1378
+ })
1379
+ };
1380
+ if (definition.views !== void 0) {
1381
+ normalized.views = [...definition.views].sort((a, b) => a.view.localeCompare(b.view)).map((view) => ({
1382
+ ...view,
1383
+ links: sortByName(view.links),
1384
+ dependencies: [...view.dependencies].sort()
1385
+ }));
1386
+ }
1387
+ return normalized;
1388
+ }
1389
+ function stableJson(value) {
1390
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1391
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1392
+ 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)}`);
1393
+ return `{${entries.join(",")}}`;
1394
+ }
1395
+ function emptyToNull(value) {
1396
+ if (value == null) return null;
1397
+ if (Array.isArray(value)) return value.length === 0 ? null : value;
1398
+ if (typeof value === "object" && Object.keys(value).length === 0) return null;
1399
+ return value;
1400
+ }
1401
+ function canonicalTable(table) {
1402
+ return {
1403
+ table: table.table,
1404
+ description: table.description ?? null,
1405
+ columns: table.columns ?? null,
1406
+ source: table.source ?? null,
1407
+ links: emptyToNull(table.links),
1408
+ indexes: emptyToNull(table.indexes),
1409
+ // insert/update are omitted rather than nulled when absent, so definitions
1410
+ // predating split policies keep the hash they were registered under.
1411
+ policies: {
1412
+ read: table.policies?.read?.source ?? null,
1413
+ write: table.policies?.write?.source ?? null,
1414
+ ...table.policies?.insert !== void 0 ? { insert: table.policies.insert.source } : {},
1415
+ ...table.policies?.update !== void 0 ? { update: table.policies.update.source } : {}
1416
+ },
1417
+ actions: emptyToNull(
1418
+ Object.fromEntries(
1419
+ Object.entries(table.actions ?? {}).map(([name, action]) => [
1420
+ name,
1421
+ {
1422
+ description: action.description ?? null,
1423
+ requires: action.requires?.source ?? null,
1424
+ params: emptyToNull(action.params),
1425
+ validates: emptyToNull(action.validates?.map((entry) => entry.source)),
1426
+ executes: action.executes,
1427
+ returns: emptyToNull(action.returns),
1428
+ audit: action.audit ?? null
1429
+ }
1430
+ ])
1431
+ )
1432
+ )
1433
+ };
1434
+ }
1435
+ function canonicalCapabilities(capabilities) {
1436
+ const { definitions, capabilities: declared } = capabilities.roles;
1437
+ const isEmpty = Object.keys(definitions).length === 0 && Object.keys(declared).length === 0 && capabilities.uses.length === 0;
1438
+ if (isEmpty) return null;
1439
+ return {
1440
+ roles: {
1441
+ definitions: Object.fromEntries(
1442
+ Object.entries(definitions).map(([key, def]) => [
1443
+ key,
1444
+ {
1445
+ ...def.description !== void 0 ? { description: def.description } : {},
1446
+ default: def.default,
1447
+ inherits: [...def.inherits].sort(),
1448
+ grants: [...def.grants].sort()
1449
+ }
1450
+ ])
1451
+ ),
1452
+ capabilities: Object.fromEntries(
1453
+ Object.entries(declared).map(([key, cap]) => [
1454
+ key,
1455
+ cap.description !== void 0 ? { description: cap.description } : {}
1456
+ ])
1457
+ )
1458
+ },
1459
+ uses: [...capabilities.uses].sort()
1460
+ };
1461
+ }
1462
+ function canonicalizeDefinition(definition) {
1463
+ return {
1464
+ namespace: definition.namespace,
1465
+ description: definition.description,
1466
+ capabilities: canonicalCapabilities(definition.capabilities),
1467
+ tables: definition.tables.map(canonicalTable)
1468
+ };
1469
+ }
1470
+ function computeDefinitionHash(definition) {
1471
+ return createHash("sha256").update(HASH_DOMAIN).update(stableJson(canonicalizeDefinition(definition))).digest("hex");
1472
+ }
1473
+
1474
+ // src/authoring/namespace.ts
1475
+ var NAMESPACE_CONFIG_BRAND = "seq-namespace-config";
1476
+ function isSeqNamespaceConfig(value) {
1477
+ return typeof value === "object" && value !== null && value.kind === NAMESPACE_CONFIG_BRAND;
1478
+ }
1479
+ function defineNamespace(input) {
1480
+ const file = activeAuthoringContext()?.currentFile;
1481
+ if (!NAMESPACE_PATTERN.test(input.namespace)) {
1482
+ throw new OrmDefinitionError(`namespace "${input.namespace}" must match ${NAMESPACE_PATTERN}`, file);
1483
+ }
1484
+ const config = {
1485
+ kind: NAMESPACE_CONFIG_BRAND,
1486
+ namespace: input.namespace,
1487
+ description: input.description,
1488
+ capabilities: input.capabilities ?? {},
1489
+ file
1490
+ };
1491
+ registerNamespaceConfig(config);
1492
+ return config;
1493
+ }
1494
+ function serializeNamespace(input) {
1495
+ const { config, tables } = input;
1496
+ const views = input.views ?? [];
1497
+ const enums = input.enums ?? [];
1498
+ const tablesByName = /* @__PURE__ */ new Map();
1499
+ for (const table of tables) {
1500
+ const existing = tablesByName.get(table.tableName);
1501
+ if (existing) {
1502
+ throw new OrmDefinitionError(
1503
+ `duplicate table "${table.tableName}" (defined in ${describeFile(existing)} and ${describeFile(table)})`,
1504
+ table.file
1505
+ );
1506
+ }
1507
+ tablesByName.set(table.tableName, table);
1508
+ }
1509
+ const viewsByName = /* @__PURE__ */ new Map();
1510
+ for (const view of views) {
1511
+ const existing = viewsByName.get(view.viewName);
1512
+ if (existing) {
1513
+ throw new OrmDefinitionError(
1514
+ `duplicate view "${view.viewName}" (defined in ${describeFile(existing)} and ${describeFile(view)})`,
1515
+ view.file
1516
+ );
1517
+ }
1518
+ viewsByName.set(view.viewName, view);
1519
+ }
1520
+ const relationsByName = /* @__PURE__ */ new Map();
1521
+ for (const relation of [...tables, ...views]) {
1522
+ const name = relationName(relation);
1523
+ const existing = relationsByName.get(name);
1524
+ if (existing) {
1525
+ throw new OrmDefinitionError(
1526
+ `duplicate relation "${name}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
1527
+ relation.file
1528
+ );
1529
+ }
1530
+ relationsByName.set(name, relation);
1531
+ }
1532
+ const enumsByName = /* @__PURE__ */ new Map();
1533
+ for (const ref of enums) {
1534
+ if (enumsByName.has(ref.name)) {
1535
+ throw new OrmDefinitionError(`duplicate enum "${ref.name}"`, config.file);
1536
+ }
1537
+ enumsByName.set(ref.name, ref);
1538
+ }
1539
+ const canonicalTables = tables.map((table) => buildCanonicalTable({ table, relationsByName, enumsByName }));
1540
+ const canonicalEnums = [...enumsByName.values()].map((ref) => ({
1541
+ name: ref.name,
1542
+ values: [...ref.values]
1543
+ }));
1544
+ const viewDrafts = views.map((view) => buildCanonicalView({ view, relationsByName, enumsByName }));
1545
+ const compiledViews = compileViewQueries({
1546
+ namespace: config.namespace,
1547
+ tables: canonicalTables,
1548
+ views: viewDrafts,
1549
+ enums: canonicalEnums,
1550
+ authoredTables: tables,
1551
+ authoredViews: views
1552
+ });
1553
+ const canonicalViews = viewDrafts.map((draft) => {
1554
+ const compiled = compiledViews.get(draft.view);
1555
+ if (!compiled) throw new OrmDefinitionError(`view "${draft.view}" did not compile`, viewsByName.get(draft.view)?.file);
1556
+ return canonicalViewSchema.parse({ ...draft, ...compiled });
1557
+ });
1558
+ assertAcyclicViewDependencies(canonicalViews);
1559
+ const candidate = {
1560
+ definition_format: CANONICAL_FORMAT,
1561
+ namespace: config.namespace,
1562
+ enums: canonicalEnums,
1563
+ tables: canonicalTables,
1564
+ capabilities: {
1565
+ ...config.capabilities.roles !== void 0 ? { roles: config.capabilities.roles } : {},
1566
+ uses: [...config.capabilities.uses ?? []]
1567
+ }
1568
+ };
1569
+ if (config.description !== void 0) candidate.description = config.description;
1570
+ if (canonicalViews.length > 0) candidate.views = canonicalViews;
1571
+ let parsed;
1572
+ try {
1573
+ parsed = canonicalDefinitionSchema.parse(candidate);
1574
+ } catch (error) {
1575
+ if (error instanceof z.ZodError) {
1576
+ throw new OrmDefinitionError(`namespace "${config.namespace}": ${formatZodIssues(error)}`, config.file);
1577
+ }
1578
+ throw error;
1579
+ }
1580
+ return normalizeCanonical(parsed);
1581
+ }
1582
+ function computeCanonicalHash(definition) {
1583
+ return createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
1584
+ }
1585
+ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1586
+ const candidate = {
1587
+ table: table.tableName,
1588
+ columns: [],
1589
+ links: [],
1590
+ indexes: [],
1591
+ policies: {},
1592
+ actions: []
1593
+ };
1594
+ if (table.config.description !== void 0) candidate.description = table.config.description;
1595
+ if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
1596
+ if (table.source !== void 0) candidate.source = table.source;
1597
+ if (table.source !== void 0) {
1598
+ if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
1599
+ throw new OrmDefinitionError(
1600
+ `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
1601
+ table.file
1602
+ );
1603
+ }
1604
+ if (table.config.cowMutations !== void 0 && table.config.cowMutations.length > 0) {
1605
+ throw new OrmDefinitionError(
1606
+ `synced table "${table.tableName}" is read-only; cow mutations belong on the overlay table`,
1607
+ table.file
1608
+ );
1609
+ }
1610
+ if (table.config.policies?.write !== void 0 || table.config.policies?.insert !== void 0 || table.config.policies?.update !== void 0) {
1611
+ throw new OrmDefinitionError(`synced table "${table.tableName}" is read-only`, table.file);
1612
+ }
1613
+ for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
1614
+ if (link.enforce) {
1615
+ throw new OrmDefinitionError(
1616
+ `synced table "${table.tableName}": link "${linkName}" cannot enforce foreign keys \u2014 its DDL is owned by the sync`,
1617
+ table.file
1618
+ );
1619
+ }
1620
+ }
1621
+ }
1622
+ const columnLevelPk = [];
1623
+ for (const [propertyName, builder] of Object.entries(table.columns)) {
1624
+ const column = builder.toCanonicalColumn(propertyName);
1625
+ const enumName = builder.enumName;
1626
+ if (enumName !== void 0 && !enumsByName.has(enumName)) {
1627
+ throw new OrmDefinitionError(
1628
+ `table "${table.tableName}": column "${propertyName}" references enum "${enumName}" which is not passed to serializeNamespace \u2014 declare it with defineEnum and include it`,
1629
+ table.file
1630
+ );
1631
+ }
1632
+ if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1633
+ candidate.columns?.push(column);
1634
+ }
1635
+ if (table.config.primaryKey !== void 0) {
1636
+ if (columnLevelPk.length > 0) {
1637
+ throw new OrmDefinitionError(
1638
+ `table "${table.tableName}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
1639
+ table.file
1640
+ );
1641
+ }
1642
+ candidate.primaryKey = table.config.primaryKey.map(
1643
+ (ref) => resolveColumnRef({ relation: table, ref, role: "primaryKey" })
1644
+ );
1645
+ } else if (columnLevelPk.length > 0) {
1646
+ candidate.primaryKey = columnLevelPk;
1647
+ }
1648
+ for (const index3 of table.config.indexes ?? []) {
1649
+ const entry = {
1650
+ columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
1651
+ };
1652
+ if (index3.unique !== void 0) entry.unique = index3.unique;
1653
+ if (index3.name !== void 0) entry.name = index3.name;
1654
+ candidate.indexes?.push(entry);
1655
+ }
1656
+ for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
1657
+ candidate.links?.push(buildCanonicalLink({ relation: table, linkName, link, relationsByName }));
1658
+ }
1659
+ const policies = {};
1660
+ if (table.config.policies?.read !== void 0) policies.read = toCanonicalPolicy(table.config.policies.read);
1661
+ if (table.config.policies?.write !== void 0) policies.write = toCanonicalPolicy(table.config.policies.write);
1662
+ if (table.config.policies?.insert !== void 0) policies.insert = toCanonicalPolicy(table.config.policies.insert);
1663
+ if (table.config.policies?.update !== void 0) policies.update = toCanonicalPolicy(table.config.policies.update);
1664
+ candidate.policies = policies;
1665
+ const cowMutations = table.config.cowMutations ?? [];
1666
+ if (cowMutations.length > 0) {
1667
+ candidate.cowMutations = cowMutations.map((mutation) => ({
1668
+ kind: mutation.kind,
1669
+ name: mutation.name,
1670
+ synced: mutation.synced,
1671
+ pk: mutation.pk,
1672
+ copy: [...mutation.copy],
1673
+ set: [...mutation.set]
1674
+ }));
1675
+ }
1676
+ try {
1677
+ return canonicalTableSchema.parse(candidate);
1678
+ } catch (error) {
1679
+ if (error instanceof z.ZodError) {
1680
+ throw new OrmDefinitionError(`table "${table.tableName}": ${formatZodIssues(error)}`, table.file);
1681
+ }
1682
+ throw error;
1683
+ }
1684
+ }
1685
+ function buildCanonicalView({
1686
+ view,
1687
+ relationsByName,
1688
+ enumsByName
1689
+ }) {
1690
+ if (view.config.policies?.read === void 0) {
1691
+ throw new OrmDefinitionError(`view "${view.viewName}" must declare policies.read`, view.file);
1692
+ }
1693
+ const columns = [];
1694
+ const columnLevelPk = [];
1695
+ for (const [propertyName, builder] of Object.entries(view.columns)) {
1696
+ const column = builder.toCanonicalColumn(propertyName);
1697
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
1698
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1699
+ }
1700
+ if (column.unique) {
1701
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1702
+ }
1703
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
1704
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare migration hints`, view.file);
1705
+ }
1706
+ const enumName = builder.enumName;
1707
+ if (enumName !== void 0 && !enumsByName.has(enumName)) {
1708
+ throw new OrmDefinitionError(
1709
+ `view "${view.viewName}": column "${propertyName}" references enum "${enumName}" which is not part of this namespace`,
1710
+ view.file
1711
+ );
1712
+ }
1713
+ if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1714
+ columns.push(column);
1715
+ }
1716
+ let primaryKey2;
1717
+ if (view.config.primaryKey !== void 0) {
1718
+ if (columnLevelPk.length > 0) {
1719
+ throw new OrmDefinitionError(
1720
+ `view "${view.viewName}": declare the logical primary key either on columns or with config.primaryKey \u2014 not both`,
1721
+ view.file
1722
+ );
1723
+ }
1724
+ primaryKey2 = view.config.primaryKey.map((ref) => resolveColumnRef({ relation: view, ref, role: "primaryKey" }));
1725
+ } else if (columnLevelPk.length > 0) {
1726
+ primaryKey2 = columnLevelPk;
1727
+ }
1728
+ const links = Object.entries(view.config.links ?? {}).map(
1729
+ ([linkName, link]) => buildCanonicalLink({ relation: view, linkName, link, relationsByName })
1730
+ );
1731
+ return {
1732
+ view: view.viewName,
1733
+ ...view.config.description !== void 0 ? { description: view.config.description } : {},
1734
+ columns,
1735
+ ...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
1736
+ links,
1737
+ policies: { read: toCanonicalPolicy(view.config.policies.read) }
1738
+ };
1739
+ }
1740
+ function buildCanonicalLink({
1741
+ relation,
1742
+ linkName,
1743
+ link,
1744
+ relationsByName
1745
+ }) {
1746
+ const ownerKind = relationKind(relation);
1747
+ const ownerName = relationName(relation);
1748
+ const fail = (message) => {
1749
+ throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
1750
+ };
1751
+ if (link.enforce && link.linkKind !== "toOne") {
1752
+ return fail("sets enforce: true, which is only supported on toOne links");
1753
+ }
1754
+ if (link.linkKind === "toPlatformUser") {
1755
+ if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
1756
+ return {
1757
+ name: linkName,
1758
+ kind: "toPlatformUser",
1759
+ on: resolveColumnRef({ relation, ref: link.on, role: `link "${linkName}" on` }),
1760
+ ...link.references !== void 0 ? { references: deriveColumnName(link.references) } : {},
1761
+ enforce: false,
1762
+ ...link.description !== void 0 ? { description: link.description } : {}
1763
+ };
1764
+ }
1765
+ const target = resolveRelationRef({ ref: link.target, fail });
1766
+ const targetName = relationName(target);
1767
+ if (!relationsByName.has(targetName)) {
1768
+ return fail(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
1769
+ }
1770
+ if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
1771
+ return fail("cannot enforce a PostgreSQL foreign key involving a view");
1772
+ }
1773
+ const owningSide = link.linkKind === "toOne" ? relation : target;
1774
+ const referencedSide = link.linkKind === "toOne" ? target : relation;
1775
+ return {
1776
+ name: linkName,
1777
+ kind: link.linkKind,
1778
+ target: targetName,
1779
+ on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1780
+ ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
1781
+ enforce: link.enforce,
1782
+ ...link.description !== void 0 ? { description: link.description } : {}
1783
+ };
1784
+ }
1785
+ function resolveRelationRef({ ref, fail }) {
1786
+ if (ref === void 0) return fail("declares no target relation");
1787
+ if (typeof ref !== "function") return ref;
1788
+ let resolved;
1789
+ try {
1790
+ resolved = ref();
1791
+ } catch (error) {
1792
+ return fail(
1793
+ `target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
1794
+ );
1795
+ }
1796
+ if (!isSeqTable(resolved) && !isSeqView(resolved)) {
1797
+ return fail("target thunk did not return a relation defined with defineTable/defineView");
1798
+ }
1799
+ return resolved;
1800
+ }
1801
+ function resolveColumnRef({ relation, ref, role }) {
1802
+ const direct = relation.columns[ref];
1803
+ if (direct) return direct.columnNameFor(ref);
1804
+ for (const [propertyName, builder] of Object.entries(relation.columns)) {
1805
+ if (builder.columnNameFor(propertyName) === ref) return ref;
1806
+ }
1807
+ const known = Object.keys(relation.columns).join(", ");
1808
+ throw new OrmDefinitionError(
1809
+ `${relationKind(relation)} "${relationName(relation)}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
1810
+ relation.file
1811
+ );
1812
+ }
1813
+ function assertAcyclicViewDependencies(views) {
1814
+ const viewNames = new Set(views.map((view) => view.view));
1815
+ const remaining = new Map(
1816
+ views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
1817
+ );
1818
+ while (remaining.size > 0) {
1819
+ const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name]) => name);
1820
+ if (ready.length === 0) {
1821
+ throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
1822
+ }
1823
+ for (const name of ready) remaining.delete(name);
1824
+ }
1825
+ }
1826
+ function toCanonicalPolicy(policy) {
1827
+ if (typeof policy === "string") return policy;
1828
+ return {
1829
+ default: policy.default,
1830
+ variants: (policy.variants ?? []).map((variant) => ({
1831
+ when: variant.when,
1832
+ ...variant.allow === true ? { allow: true } : {},
1833
+ ...variant.policy !== void 0 ? { policy: variant.policy } : {}
1834
+ }))
1835
+ };
1836
+ }
1837
+ function describeFile(relation) {
1838
+ return relation.file ?? "<unknown file>";
1839
+ }
1840
+ function formatZodIssues(error) {
1841
+ return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
1842
+ }
1843
+ var SYNCED_CONTRACT_HASH_DOMAIN = "sequence-orm-synced-contract\0";
1844
+ var ORM_TO_PIPELINE_TYPE = {
1845
+ uuid: "uuid",
1846
+ text: "string",
1847
+ integer: "int",
1848
+ bigint: "long",
1849
+ numeric: "numeric",
1850
+ double: "double",
1851
+ boolean: "boolean",
1852
+ timestamptz: "timestamp",
1853
+ date: "date",
1854
+ jsonb: "json"
1855
+ };
1856
+ function isColumnType(value) {
1857
+ return typeof value === "string" && value in ORM_TO_PIPELINE_TYPE;
1858
+ }
1859
+ function syncedContractSurface({
1860
+ namespace,
1861
+ table,
1862
+ columns,
1863
+ primary_key
1864
+ }) {
1865
+ return {
1866
+ namespace,
1867
+ table,
1868
+ columns: Object.fromEntries(
1869
+ Object.entries(columns).map(([name, column]) => [
1870
+ name,
1871
+ { type: column.type, nullable: column.nullable, key: column.key }
1872
+ ])
1873
+ ),
1874
+ primary_key: [...primary_key]
1875
+ };
1876
+ }
1877
+ function computeSyncedContractHash(surface) {
1878
+ return createHash("sha256").update(SYNCED_CONTRACT_HASH_DOMAIN).update(stableJson(surface)).digest("hex");
1879
+ }
1880
+ function extractOrmSyncedTableContract({
1881
+ namespace,
1882
+ table
1883
+ }) {
1884
+ if (!NAMESPACE_PATTERN.test(namespace)) {
1885
+ throw new OrmDefinitionError(
1886
+ `namespace "${namespace}" must match ${NAMESPACE_PATTERN}`
1887
+ );
1888
+ }
1889
+ if (table.source === void 0) {
1890
+ throw new OrmDefinitionError(
1891
+ `table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires source: gold.*`
1892
+ );
1893
+ }
1894
+ if (table.columns.length === 0) {
1895
+ throw new OrmDefinitionError(
1896
+ `synced table "${table.table}" has no typed columns \u2014 declare a typed defineSyncedTable contract before exporting`
1897
+ );
1898
+ }
1899
+ const primaryKey2 = table.primaryKey;
1900
+ if (primaryKey2 === void 0 || primaryKey2.length === 0) {
1901
+ throw new OrmDefinitionError(
1902
+ `synced table "${table.table}" has no primary key \u2014 typed synced contracts require a primary key`
1903
+ );
1904
+ }
1905
+ const pkSet = new Set(primaryKey2);
1906
+ const columns = {};
1907
+ for (const column of table.columns) {
1908
+ if (!isColumnType(column.type)) {
1909
+ const enumName = typeof column.type === "object" && column.type !== null && "enum" in column.type ? column.type.enum : String(column.type);
1910
+ throw new OrmDefinitionError(
1911
+ `synced table "${table.table}": column "${column.columnName}" has enum/type '${enumName}', which has no pipeline-type mapping \u2014 use an ORM-mappable scalar`
1912
+ );
1913
+ }
1914
+ columns[column.columnName] = {
1915
+ type: ORM_TO_PIPELINE_TYPE[column.type],
1916
+ nullable: column.nullable,
1917
+ key: pkSet.has(column.columnName)
1918
+ };
1919
+ }
1920
+ for (const key of primaryKey2) {
1921
+ if (!(key in columns)) {
1922
+ throw new OrmDefinitionError(
1923
+ `synced table "${table.table}": primary key column "${key}" is not declared in columns`
1924
+ );
1925
+ }
1926
+ }
1927
+ const surface = syncedContractSurface({
1928
+ namespace,
1929
+ table: table.table,
1930
+ columns,
1931
+ primary_key: primaryKey2
1932
+ });
1933
+ return {
1934
+ namespace,
1935
+ table: table.table,
1936
+ columns,
1937
+ primary_key: [...primaryKey2],
1938
+ content_hash: computeSyncedContractHash(surface)
1939
+ };
1940
+ }
1941
+ var NAMESPACE_CONFIG_FILE = "sequence.config.ts";
1942
+ var SCHEMA_DIR = "schema";
1943
+ async function loadNamespaceFromDir(dir, options = {}) {
1944
+ const root = await realpath(dir).catch(() => {
1945
+ throw new OrmDefinitionError(`namespace directory not found: ${dir}`);
1946
+ });
1947
+ const configPath = join(root, NAMESPACE_CONFIG_FILE);
1948
+ const configStat = await stat(configPath).catch(() => void 0);
1949
+ if (!configStat?.isFile()) {
1950
+ throw new OrmDefinitionError(`${NAMESPACE_CONFIG_FILE} not found in ${dir}`);
1951
+ }
1952
+ const schemaFiles = await listSchemaFiles(root);
1953
+ const files = [NAMESPACE_CONFIG_FILE, ...schemaFiles];
1954
+ purgeModuleCache(root);
1955
+ const jiti = createJiti(import.meta.url, {
1956
+ interopDefault: true,
1957
+ fsCache: false,
1958
+ ...options.ormModulePath !== void 0 ? { alias: { "@sequenceholdings/orm": options.ormModulePath } } : {}
1959
+ });
1960
+ const context = beginAuthoringContext();
1961
+ try {
1962
+ for (const file of files) {
1963
+ context.currentFile = file;
1964
+ try {
1965
+ await jiti.import(join(root, file));
1966
+ } catch (error) {
1967
+ if (error instanceof Error && error.name === "OrmDefinitionError") throw error;
1968
+ throw new OrmDefinitionError(
1969
+ `failed to load: ${error instanceof Error ? error.message : String(error)}`,
1970
+ file
1971
+ );
1972
+ }
1973
+ }
1974
+ } finally {
1975
+ context.currentFile = void 0;
1976
+ endAuthoringContext(context);
1977
+ }
1978
+ const configs = [];
1979
+ for (const entry of context.configs) {
1980
+ if (isSeqNamespaceConfig(entry.value)) configs.push({ value: entry.value, file: entry.file });
1981
+ }
1982
+ const configEntry = configs[0];
1983
+ if (!configEntry) {
1984
+ throw new OrmDefinitionError(
1985
+ "no namespace config registered \u2014 sequence.config.ts must call defineNamespace(...) at import time",
1986
+ NAMESPACE_CONFIG_FILE
1987
+ );
1988
+ }
1989
+ if (configs.length > 1) {
1990
+ const where = configs.map((entry) => entry.file ?? "<unknown file>").join(", ");
1991
+ throw new OrmDefinitionError(
1992
+ `defineNamespace called ${configs.length} times (${where}) \u2014 declare exactly one namespace config`
1993
+ );
1994
+ }
1995
+ const tables = [];
1996
+ for (const entry of context.tables) {
1997
+ if (isSeqTable(entry.value)) tables.push(entry.value);
1998
+ }
1999
+ const views = [];
2000
+ for (const entry of context.views) {
2001
+ if (isSeqView(entry.value)) views.push(entry.value);
2002
+ }
2003
+ if (tables.length === 0 && views.length === 0) {
2004
+ throw new OrmDefinitionError(
2005
+ `no relations registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) or defineView(...) at import time`
2006
+ );
2007
+ }
2008
+ const enums = [];
2009
+ for (const entry of context.enums) {
2010
+ if (isEnumRef(entry.value)) enums.push(entry.value);
2011
+ }
2012
+ const canonical = serializeNamespace({ config: configEntry.value, tables, views, enums });
2013
+ return { canonical, hash: computeCanonicalHash(canonical), files };
2014
+ }
2015
+ async function listSchemaFiles(root) {
2016
+ const schemaDir = join(root, SCHEMA_DIR);
2017
+ const entries = await readdir(schemaDir, { withFileTypes: true }).catch(() => []);
2018
+ return entries.filter(
2019
+ (entry) => entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.startsWith(".")
2020
+ ).map((entry) => join(SCHEMA_DIR, entry.name)).sort();
2021
+ }
2022
+ function purgeModuleCache(root) {
2023
+ const require2 = createRequire(import.meta.url);
2024
+ const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
2025
+ for (const key of Object.keys(require2.cache)) {
2026
+ if (key.startsWith(prefix)) delete require2.cache[key];
2027
+ }
2028
+ }
9
2029
 
10
2030
  // src/codegen/types.ts
11
2031
  var WIRE_TYPES = {
@@ -27,13 +2047,17 @@ function generateNamespaceTypes(definition) {
27
2047
  `export type ${typesInterfaceName(definition.namespace)} = {`
28
2048
  ];
29
2049
  for (const table of definition.tables) {
30
- if (table.source) {
2050
+ const columns = Object.entries(table.columns ?? {});
2051
+ if (table.source && columns.length === 0) {
31
2052
  lines.push(` /** Synced from ${table.source} \u2014 columns reflect the gold source. */`);
32
2053
  lines.push(` ${table.table}: Record<string, unknown>`);
33
2054
  continue;
34
2055
  }
2056
+ if (table.source) {
2057
+ lines.push(` /** Synced from ${table.source} \u2014 read-only typed contract. */`);
2058
+ }
35
2059
  lines.push(` ${table.table}: {`);
36
- for (const [name, column] of Object.entries(table.columns ?? {})) {
2060
+ for (const [name, column] of columns) {
37
2061
  if (column.deprecated) continue;
38
2062
  lines.push(` ${name}: ${columnWireType(column)}`);
39
2063
  }
@@ -84,43 +2108,6 @@ function columnWireType(column) {
84
2108
  return notNull || base === "unknown" ? base : `${base} | null`;
85
2109
  }
86
2110
 
87
- // src/constants.ts
88
- var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
89
- var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
90
- var MAX_IDENTIFIER_LENGTH = 63;
91
- var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
92
- var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
93
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
94
- var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
95
- var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
96
- var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
97
- var ORM_SCHEMA_PREFIX = "orm_";
98
- var CONSUMER_ROLE_SUFFIX = "_consumer";
99
- var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
100
- var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
101
- var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
102
- var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
103
- var HASH_DOMAIN = "sequence-orm-definitions\0";
104
- var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
105
- var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
106
- var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
107
-
108
- // src/errors.ts
109
- var OrmDefinitionError = class extends Error {
110
- constructor(message, file) {
111
- super(file ? `${file}: ${message}` : message);
112
- this.file = file;
113
- }
114
- name = "OrmDefinitionError";
115
- };
116
- var PredicateParseError = class extends Error {
117
- constructor(message, position) {
118
- super(`${message} (at position ${position})`);
119
- this.position = position;
120
- }
121
- name = "PredicateParseError";
122
- };
123
-
124
2111
  // src/predicate/ast.ts
125
2112
  var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
126
2113
  function walkPredicate(predicate, visit) {
@@ -159,7 +2146,7 @@ var tableReferenceSchema = z.string().refine(
159
2146
  var isQualifiedReference = (reference) => reference.includes(".");
160
2147
 
161
2148
  // src/schema/columns.ts
162
- var COLUMN_TYPES = [
2149
+ var COLUMN_TYPES2 = [
163
2150
  "uuid",
164
2151
  "text",
165
2152
  "integer",
@@ -171,7 +2158,7 @@ var COLUMN_TYPES = [
171
2158
  "date",
172
2159
  "jsonb"
173
2160
  ];
174
- var columnTypeSchema = z.enum(COLUMN_TYPES);
2161
+ var columnTypeSchema2 = z.enum(COLUMN_TYPES2);
175
2162
  var FUNCTION_DEFAULTS = {
176
2163
  "gen_random_uuid()": ["uuid"],
177
2164
  "now()": ["timestamptz", "date"]
@@ -200,7 +2187,7 @@ function defaultIssue(value, type) {
200
2187
  return null;
201
2188
  }
202
2189
  var columnSchema = z.strictObject({
203
- type: columnTypeSchema,
2190
+ type: columnTypeSchema2,
204
2191
  primary_key: z.boolean().optional(),
205
2192
  nullable: z.boolean().optional(),
206
2193
  unique: z.boolean().optional(),
@@ -467,7 +2454,7 @@ var NAMED_PARAM_PATTERN = /(?<!:):([a-z][a-z0-9_]*)/g;
467
2454
  var QUOTED_STRING_PATTERN = /'(?:[^']|'')*'/g;
468
2455
  var SQL_COMMENT_PATTERN = /--|\/\*/;
469
2456
  var actionParamSchema = z.strictObject({
470
- type: columnTypeSchema,
2457
+ type: columnTypeSchema2,
471
2458
  optional: z.boolean().optional(),
472
2459
  description: z.string().optional()
473
2460
  });
@@ -500,7 +2487,7 @@ var actionSchema = z.strictObject({
500
2487
  // function returns those rows (typed in the client); absent → the action
501
2488
  // reports rows_affected only. The column names/types must match the
502
2489
  // RETURNING list, which Postgres enforces at CREATE FUNCTION.
503
- returns: z.record(identifierSchema, columnTypeSchema).optional(),
2490
+ returns: z.record(identifierSchema, columnTypeSchema2).optional(),
504
2491
  audit: auditSchema.optional()
505
2492
  }).superRefine((action, ctx) => {
506
2493
  const params = Object.keys(action.params ?? {});
@@ -517,18 +2504,18 @@ var actionSchema = z.strictObject({
517
2504
  if (action.returns && Object.keys(action.returns).length === 0) {
518
2505
  ctx.addIssue({ code: "custom", message: "returns must declare at least one column", path: ["returns"] });
519
2506
  }
520
- for (const [index2, entry] of (action.validates ?? []).entries()) {
2507
+ for (const [index3, entry] of (action.validates ?? []).entries()) {
521
2508
  if (!params.includes(entry.param)) {
522
2509
  ctx.addIssue({
523
2510
  code: "custom",
524
2511
  message: `validates references undeclared param '${entry.param}'`,
525
- path: ["validates", index2]
2512
+ path: ["validates", index3]
526
2513
  });
527
2514
  }
528
2515
  }
529
2516
  });
530
- function extractNamedParams(sql2) {
531
- const withoutStrings = sql2.replace(QUOTED_STRING_PATTERN, "''");
2517
+ function extractNamedParams(sql4) {
2518
+ const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
532
2519
  const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
533
2520
  return [...new Set(names)];
534
2521
  }
@@ -561,9 +2548,9 @@ function actionWriteTargets(action) {
561
2548
  function executesIssues(executes, declaredParams, hasReturns) {
562
2549
  const issues = [];
563
2550
  const statements = actionStatementSources(executes);
564
- statements.forEach((statement, index2) => {
2551
+ statements.forEach((statement, index3) => {
565
2552
  const withoutStrings = statement.replace(QUOTED_STRING_PATTERN, "''");
566
- const at = statements.length > 1 ? ` (statement ${index2 + 1})` : "";
2553
+ const at = statements.length > 1 ? ` (statement ${index3 + 1})` : "";
567
2554
  if (!EXECUTES_VERB_PATTERN.test(statement)) issues.push(`executes must be INSERT, UPDATE, or DELETE${at}`);
568
2555
  if (withoutStrings.replace(/;\s*$/, "").includes(";")) issues.push(`each executes entry must be a single statement${at}`);
569
2556
  if (SQL_COMMENT_PATTERN.test(withoutStrings)) issues.push(`executes must not contain SQL comments${at}`);
@@ -573,7 +2560,7 @@ function executesIssues(executes, declaredParams, hasReturns) {
573
2560
  if (BARE_CLAIM_CALL_PATTERN.test(withoutStrings.replace(MASKED_CLAIM_CALL_PATTERN, ""))) {
574
2561
  issues.push(`executes has a malformed claim()/capability() call \u2014 use e.g. claim('sub')${at}`);
575
2562
  }
576
- if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index2 === statements.length - 1)) {
2563
+ if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index3 === statements.length - 1)) {
577
2564
  issues.push(
578
2565
  hasReturns ? `only the last executes statement may use RETURNING${at}` : `executes must not use RETURNING unless the action declares returns${at}`
579
2566
  );
@@ -598,11 +2585,13 @@ var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
598
2585
  // src/compiler/sql.ts
599
2586
  var CLAIMS_SETTING = "request.jwt.claims";
600
2587
  var CAPABILITIES_CLAIM_KEY = "capabilities";
2588
+ var ROLES_CLAIM_KEY = "roles";
601
2589
  var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
602
2590
  var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
603
2591
  var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
604
2592
  var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
605
2593
  var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
2594
+ var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
606
2595
 
607
2596
  // src/compiler/predicates.ts
608
2597
  var SQL_COMPARE_OPS = {
@@ -683,8 +2672,8 @@ var actionParamArg = (name) => `p_${name}`;
683
2672
  function compileActionStatements(action, renderParam = actionParamArg) {
684
2673
  return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
685
2674
  }
686
- function compileStatement(sql2, renderParam) {
687
- const compiled = sql2.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
2675
+ function compileStatement(sql4, renderParam) {
2676
+ const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
688
2677
  if (claimKey !== void 0) {
689
2678
  if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
690
2679
  return claimExpression(claimKey);
@@ -716,6 +2705,118 @@ function referencedExecuteCapabilities(action) {
716
2705
  return keys;
717
2706
  }
718
2707
 
2708
+ // src/compiler/pred-ir-sql.ts
2709
+ function compilePredIrToSql(pred, options = {}) {
2710
+ switch (pred.kind) {
2711
+ case "const":
2712
+ return pred.value ? "TRUE" : "FALSE";
2713
+ case "and":
2714
+ return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" AND ")})`;
2715
+ case "or":
2716
+ return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" OR ")})`;
2717
+ case "not":
2718
+ return `(NOT ${compilePredIrToSql(pred.operand, options)})`;
2719
+ case "cmp":
2720
+ return compileCmp(pred.op, pred.column, pred.value, options);
2721
+ case "capability":
2722
+ return `(${capabilityExpression(pred.key)})`;
2723
+ case "role":
2724
+ return `(${roleExpression(pred.key)})`;
2725
+ case "exists":
2726
+ case "aggPredicate":
2727
+ case "dynamicFilter":
2728
+ throw new OrmDefinitionError(
2729
+ `synced view read policy cannot use '${pred.kind}' \u2014 use row filters and claim/capability/role checks only`
2730
+ );
2731
+ default: {
2732
+ const exhausted = pred;
2733
+ throw new OrmDefinitionError(`unsupported synced view predicate kind '${String(exhausted)}'`);
2734
+ }
2735
+ }
2736
+ }
2737
+ function physicalColumn(logical, options) {
2738
+ return options.columnNames?.[logical] ?? logical;
2739
+ }
2740
+ function compileCmp(op, column, value, options) {
2741
+ const physical = physicalColumn(column, options);
2742
+ const declared = options.columnTypes?.[physical] ?? options.columnTypes?.[column];
2743
+ const isEnum = typeof declared === "object" && declared !== null;
2744
+ const col = isEnum ? `${quoteIdentifier(physical)}::text` : quoteIdentifier(physical);
2745
+ const columnType = isEnum ? "text" : declared;
2746
+ switch (op) {
2747
+ case "eq":
2748
+ return compileEquality(col, value, columnType, false);
2749
+ case "neq":
2750
+ return compileEquality(col, value, columnType, true);
2751
+ case "gt":
2752
+ return `${col} > ${compileValue(value, columnType)}`;
2753
+ case "gte":
2754
+ return `${col} >= ${compileValue(value, columnType)}`;
2755
+ case "lt":
2756
+ return `${col} < ${compileValue(value, columnType)}`;
2757
+ case "lte":
2758
+ return `${col} <= ${compileValue(value, columnType)}`;
2759
+ case "in":
2760
+ return `${col} = ANY(${compileArrayValue(value, columnType)})`;
2761
+ case "nin":
2762
+ return `${col} <> ALL(${compileArrayValue(value, columnType)})`;
2763
+ case "like":
2764
+ return `${col} LIKE ${compileValue(value, columnType)}`;
2765
+ case "ilike":
2766
+ return `${col} ILIKE ${compileValue(value, columnType)}`;
2767
+ case "isNull":
2768
+ return value.kind === "literal" && value.value === true ? `${col} IS NULL` : `${col} IS NOT NULL`;
2769
+ case "contains":
2770
+ case "hasKey":
2771
+ throw new OrmDefinitionError(`synced view read policy cannot use json operator '${op}'`);
2772
+ default: {
2773
+ const exhausted = op;
2774
+ throw new OrmDefinitionError(`unsupported synced view comparison '${String(exhausted)}'`);
2775
+ }
2776
+ }
2777
+ }
2778
+ function compileEquality(col, value, columnType, negated, options) {
2779
+ if (value.kind === "literal" && value.value === null) {
2780
+ return negated ? `${col} IS NOT NULL` : `${col} IS NULL`;
2781
+ }
2782
+ const rhs = compileValue(value, columnType);
2783
+ return negated ? `${col} <> ${rhs}` : `${col} = ${rhs}`;
2784
+ }
2785
+ function compileValue(value, columnType, _options) {
2786
+ switch (value.kind) {
2787
+ case "literal": {
2788
+ const literal = value.value;
2789
+ if (literal === null) return "NULL";
2790
+ if (typeof literal === "string") return quoteLiteral(literal);
2791
+ if (typeof literal === "boolean") return literal ? "TRUE" : "FALSE";
2792
+ return String(literal);
2793
+ }
2794
+ case "claim": {
2795
+ const expression = claimExpression(value.key);
2796
+ return columnType && columnType !== "text" ? `(${expression})::${sqlTypeName(columnType)}` : expression;
2797
+ }
2798
+ case "variable":
2799
+ throw new OrmDefinitionError("synced view read policy cannot reference operation variables");
2800
+ default: {
2801
+ const exhausted = value;
2802
+ throw new OrmDefinitionError(`unsupported synced view value kind '${String(exhausted)}'`);
2803
+ }
2804
+ }
2805
+ }
2806
+ function compileArrayValue(value, columnType, _options) {
2807
+ if (value.kind !== "literal" || !Array.isArray(value.value)) {
2808
+ throw new OrmDefinitionError("synced view IN/NIN comparisons require literal arrays");
2809
+ }
2810
+ const entries = value.value.map((entry) => {
2811
+ if (entry === null) return "NULL";
2812
+ if (typeof entry === "string") return quoteLiteral(entry);
2813
+ if (typeof entry === "boolean") return entry ? "TRUE" : "FALSE";
2814
+ return String(entry);
2815
+ });
2816
+ const array = `ARRAY[${entries.join(", ")}]`;
2817
+ return columnType ? `${array}::${sqlTypeName(columnType)}[]` : array;
2818
+ }
2819
+
719
2820
  // src/engine/actions.ts
720
2821
  var ACTION_REQUIRES_DENIED_SQLSTATE = "ORMRQ";
721
2822
  var ACTION_VALIDATES_FAILED_SQLSTATE = "ORMVL";
@@ -737,9 +2838,9 @@ function actionPrepareChecks(definition) {
737
2838
  const order = actionParamOrder(action);
738
2839
  const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
739
2840
  const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
740
- compiled.forEach((sql2, index2) => {
741
- const checked = action.returns && index2 === compiled.length - 1 ? returnProjection(action.returns, sql2) : sql2;
742
- statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index2}`)}${signature} AS ${checked}`);
2841
+ compiled.forEach((sql4, index3) => {
2842
+ const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
2843
+ statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
743
2844
  });
744
2845
  }
745
2846
  return statements;
@@ -854,10 +2955,12 @@ var indexSchema = z.union([
854
2955
  unique: z.boolean().optional(),
855
2956
  name: identifierSchema.optional()
856
2957
  })
857
- ]).transform((index2) => Array.isArray(index2) ? { columns: index2 } : index2);
2958
+ ]).transform((index3) => Array.isArray(index3) ? { columns: index3 } : index3);
858
2959
  var policiesSchema = z.strictObject({
859
2960
  read: parsedPredicateSchema.optional(),
860
- write: parsedPredicateSchema.optional()
2961
+ write: parsedPredicateSchema.optional(),
2962
+ insert: parsedPredicateSchema.optional(),
2963
+ update: parsedPredicateSchema.optional()
861
2964
  });
862
2965
  var tableFileSchema = z.strictObject({
863
2966
  namespace: namespaceSchema,
@@ -877,32 +2980,64 @@ var tableFileSchema = z.strictObject({
877
2980
  if (RESERVED_COLUMN_NAMES.has(name)) {
878
2981
  ctx.addIssue({
879
2982
  code: "custom",
880
- message: `column '${name}' collides with the Data API's reserved query parameters (select, order, limit, offset) \u2014 it could never be filtered`,
881
- path: ["columns", name]
2983
+ message: `column '${name}' collides with the Data API's reserved query parameters (select, order, limit, offset) \u2014 it could never be filtered`,
2984
+ path: ["columns", name]
2985
+ });
2986
+ }
2987
+ }
2988
+ if (table.source) validateSyncedTable(table, ctx);
2989
+ else validateColumnTable(table, ctx);
2990
+ validateLinks(table, ctx);
2991
+ validatePolicyColumns(table, ctx);
2992
+ validateDerivedNameLengths(table, ctx);
2993
+ });
2994
+ var indexName = (table, definition) => definition.name ?? `ix_${table}_${definition.columns.join("_")}`;
2995
+ var policyName = (table, suffix) => `${table}_${suffix}`;
2996
+ var foreignKeyName = (table, link) => `fk_${table}_${link}`;
2997
+ function validateSyncedTable(table, ctx) {
2998
+ const forbidden = [
2999
+ ["actions", "synced tables are read-only; actions are not allowed"],
3000
+ ["indexes", "synced tables are system-owned; indexes are not allowed"]
3001
+ ];
3002
+ for (const [field, message] of forbidden) {
3003
+ if (table[field] !== void 0) ctx.addIssue({ code: "custom", message, path: [field] });
3004
+ }
3005
+ if (table.policies?.write) {
3006
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
3007
+ }
3008
+ if (table.policies?.insert) {
3009
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
3010
+ }
3011
+ if (table.policies?.update) {
3012
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
3013
+ }
3014
+ const columns = table.columns ?? {};
3015
+ const names = Object.keys(columns);
3016
+ if (names.length === 0) return;
3017
+ const primaryKeys = names.filter((name) => columns[name]?.primary_key);
3018
+ if (primaryKeys.length !== 1) {
3019
+ ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
3020
+ }
3021
+ validateRenames(columns, names, ctx);
3022
+ for (const name of names) {
3023
+ const column = columns[name];
3024
+ if (!column) continue;
3025
+ const cast = column.cast ? castParts(column.cast) : null;
3026
+ if (column.cast && cast?.column !== name) {
3027
+ ctx.addIssue({
3028
+ code: "custom",
3029
+ message: "cast must reference the column it is declared on",
3030
+ path: ["columns", name, "cast"]
3031
+ });
3032
+ }
3033
+ if (cast && cast.type !== column.type) {
3034
+ ctx.addIssue({
3035
+ code: "custom",
3036
+ message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
3037
+ path: ["columns", name, "cast"]
882
3038
  });
883
3039
  }
884
3040
  }
885
- if (table.source) validateSyncedTable(table, ctx);
886
- else validateColumnTable(table, ctx);
887
- validateLinks(table, ctx);
888
- validatePolicyColumns(table, ctx);
889
- validateDerivedNameLengths(table, ctx);
890
- });
891
- var indexName = (table, definition) => definition.name ?? `ix_${table}_${definition.columns.join("_")}`;
892
- var policyName = (table, suffix) => `${table}_${suffix}`;
893
- var foreignKeyName = (table, link) => `fk_${table}_${link}`;
894
- function validateSyncedTable(table, ctx) {
895
- const forbidden = [
896
- ["columns", "synced tables reflect their source; columns are not declared"],
897
- ["actions", "synced tables are read-only; actions are not allowed"],
898
- ["indexes", "synced tables are system-owned; indexes are not allowed"]
899
- ];
900
- for (const [field, message] of forbidden) {
901
- if (table[field] !== void 0) ctx.addIssue({ code: "custom", message, path: [field] });
902
- }
903
- if (table.policies?.write) {
904
- ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
905
- }
906
3041
  }
907
3042
  function validateRenames(columns, names, ctx) {
908
3043
  const renameSources = /* @__PURE__ */ new Map();
@@ -958,15 +3093,15 @@ function validateColumnTable(table, ctx) {
958
3093
  });
959
3094
  }
960
3095
  }
961
- for (const [position, index2] of (table.indexes ?? []).entries()) {
962
- if (new Set(index2.columns).size !== index2.columns.length) {
3096
+ for (const [position, index3] of (table.indexes ?? []).entries()) {
3097
+ if (new Set(index3.columns).size !== index3.columns.length) {
963
3098
  ctx.addIssue({
964
3099
  code: "custom",
965
3100
  message: "index names a column more than once",
966
3101
  path: ["indexes", position]
967
3102
  });
968
3103
  }
969
- for (const column of index2.columns) {
3104
+ for (const column of index3.columns) {
970
3105
  if (!names.includes(column)) {
971
3106
  ctx.addIssue({
972
3107
  code: "custom",
@@ -980,7 +3115,7 @@ function validateColumnTable(table, ctx) {
980
3115
  function validateLinks(table, ctx) {
981
3116
  const columnNames = Object.keys(table.columns ?? {});
982
3117
  for (const [name, link] of Object.entries(table.links ?? {})) {
983
- if (!table.source && !columnNames.includes(link.on)) {
3118
+ if (columnNames.length > 0 && !columnNames.includes(link.on)) {
984
3119
  ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
985
3120
  }
986
3121
  if (link.enforce && link.to.includes(".")) {
@@ -1008,8 +3143,8 @@ function validateDerivedNameLengths(table, ctx) {
1008
3143
  path: ["table"]
1009
3144
  });
1010
3145
  }
1011
- for (const [position, index2] of (table.indexes ?? []).entries()) {
1012
- const name = indexName(table.table, index2);
3146
+ for (const [position, index3] of (table.indexes ?? []).entries()) {
3147
+ const name = indexName(table.table, index3);
1013
3148
  if (name.length > MAX_IDENTIFIER_LENGTH) {
1014
3149
  ctx.addIssue({
1015
3150
  code: "custom",
@@ -1053,10 +3188,10 @@ var TYPE_GROUPS = {
1053
3188
  };
1054
3189
  var columnTypeGroup = (type) => TYPE_GROUPS[type];
1055
3190
  function validatePolicyColumns(table, ctx) {
1056
- if (table.source) return;
1057
3191
  const columns = table.columns ?? {};
1058
3192
  const columnNames = Object.keys(columns);
1059
- for (const kind of ["read", "write"]) {
3193
+ if (columnNames.length === 0) return;
3194
+ for (const kind of ["read", "write", "insert", "update"]) {
1060
3195
  const predicate = table.policies?.[kind];
1061
3196
  if (!predicate) continue;
1062
3197
  for (const column of referencedColumns(predicate.ast)) {
@@ -1133,19 +3268,339 @@ function policyStatements(schema, table) {
1133
3268
  if (read) {
1134
3269
  statements.push(`CREATE POLICY ${policyName2("read")} ON ${qualified} FOR SELECT USING (${compilePredicate(read.ast, columnTypes)})`);
1135
3270
  }
1136
- const write = table.policies?.write;
1137
- if (write) {
1138
- const predicate = compilePredicate(write.ast, columnTypes);
1139
- statements.push(`CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${predicate})`);
3271
+ const insertPolicy = table.policies?.insert ?? table.policies?.write;
3272
+ const updatePolicy = table.policies?.update ?? table.policies?.write;
3273
+ if (insertPolicy) {
3274
+ statements.push(
3275
+ `CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${compilePredicate(insertPolicy.ast, columnTypes)})`
3276
+ );
3277
+ }
3278
+ if (updatePolicy) {
3279
+ const predicate = compilePredicate(updatePolicy.ast, columnTypes);
1140
3280
  statements.push(
1141
3281
  `CREATE POLICY ${policyName2("update")} ON ${qualified} FOR UPDATE USING (${predicate}) WITH CHECK (${predicate})`
1142
3282
  );
1143
3283
  statements.push(`CREATE POLICY ${policyName2("delete")} ON ${qualified} FOR DELETE USING (${predicate})`);
1144
3284
  }
1145
- return statements;
3285
+ return statements;
3286
+ }
3287
+ var columnTables = (definition) => definition.tables.filter((table) => !table.source);
3288
+ var syncedTables = (definition) => definition.tables.filter((table) => !!table.source);
3289
+ var SYNCED_VIEW_EXECUTION_OWNER_ROLE = "orm_exec";
3290
+ function syncedViewStatements({
3291
+ definition,
3292
+ backings,
3293
+ readPolicyIrByTable,
3294
+ columnNamesByTable,
3295
+ columnTypesByTable,
3296
+ executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
3297
+ }) {
3298
+ const schema = quoteIdentifier(ormSchemaName(definition.namespace));
3299
+ const role = quoteIdentifier(consumerRoleName(definition.namespace));
3300
+ const owner = quoteIdentifier(executionOwnerRole);
3301
+ const backingMap = /* @__PURE__ */ new Map();
3302
+ for (const backing of backings) {
3303
+ const existing = backingMap.get(backing.table);
3304
+ if (existing) {
3305
+ throw new OrmDefinitionError(
3306
+ `duplicate synced backing for table "${backing.table}" ('${existing.physicalRelation}' and '${backing.physicalRelation}') \u2014 refusing to materialize an ambiguous authorization boundary`
3307
+ );
3308
+ }
3309
+ backingMap.set(backing.table, backing);
3310
+ }
3311
+ const statements = [];
3312
+ for (const table of syncedTables(definition)) {
3313
+ const backing = backingMap.get(table.table);
3314
+ if (!backing) continue;
3315
+ const viewName = `${schema}.${quoteIdentifier(table.table)}`;
3316
+ const physical = splitPhysicalRelation(backing.physicalRelation);
3317
+ const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
3318
+ const columnTypes = mergeSyncedViewColumnTypes({
3319
+ table,
3320
+ canonicalColumnTypes: columnTypesByTable?.get(table.table),
3321
+ backingColumnTypes: backing.columnTypes,
3322
+ columnNames: columnNamesByTable?.get(table.table)
3323
+ });
3324
+ const readPolicyIr = readPolicyIrByTable?.get(table.table);
3325
+ const readPolicy = table.policies?.read;
3326
+ const whereClause = readPolicyIr ? ` WHERE ${compilePredIrToSql(readPolicyIr, {
3327
+ columnTypes,
3328
+ columnNames: columnNamesByTable?.get(table.table)
3329
+ })}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
3330
+ statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical.schema)} TO ${owner}`);
3331
+ statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
3332
+ statements.push(
3333
+ `CREATE OR REPLACE VIEW ${viewName} AS SELECT * FROM ${physicalTarget}${whereClause}`
3334
+ );
3335
+ statements.push(`GRANT SELECT ON ${viewName} TO ${role}`);
3336
+ }
3337
+ return statements;
3338
+ }
3339
+ function mergeSyncedViewColumnTypes({
3340
+ table,
3341
+ canonicalColumnTypes,
3342
+ backingColumnTypes,
3343
+ columnNames
3344
+ }) {
3345
+ const fromTable = {};
3346
+ for (const [name, def] of Object.entries(table.columns ?? {})) {
3347
+ if (def?.type) fromTable[name] = def.type;
3348
+ }
3349
+ if (columnNames) {
3350
+ for (const [logical, physical] of Object.entries(columnNames)) {
3351
+ const declared = fromTable[physical] ?? fromTable[logical];
3352
+ if (!declared) continue;
3353
+ fromTable[physical] = declared;
3354
+ fromTable[logical] = declared;
3355
+ }
3356
+ }
3357
+ const canonicalEnums = Object.fromEntries(
3358
+ Object.entries(canonicalColumnTypes ?? {}).filter(([, type]) => typeof type === "object")
3359
+ );
3360
+ return { ...fromTable, ...canonicalColumnTypes ?? {}, ...backingColumnTypes ?? {}, ...canonicalEnums };
3361
+ }
3362
+ function narrowToV1ColumnTypes(columnTypes) {
3363
+ return Object.fromEntries(
3364
+ Object.entries(columnTypes).map(([column, type]) => [column, typeof type === "object" ? "text" : type])
3365
+ );
3366
+ }
3367
+ function splitPhysicalRelation(physicalRelation) {
3368
+ const parts = physicalRelation.split(".");
3369
+ if (parts.length === 1 && parts[0]) return { schema: "public", table: parts[0] };
3370
+ if (parts.length === 2 && parts[0] && parts[1]) return { schema: parts[0], table: parts[1] };
3371
+ throw new OrmDefinitionError(
3372
+ `synced view physical relation must be "table" or "schema.table" (got "${physicalRelation}")`
3373
+ );
3374
+ }
3375
+ function syncedViewDropStatements({
3376
+ schemaName,
3377
+ tables
3378
+ }) {
3379
+ const schema = quoteIdentifier(schemaName);
3380
+ return tables.map(
3381
+ (table) => `DROP VIEW IF EXISTS ${schema}.${quoteIdentifier(table)} CASCADE`
3382
+ );
3383
+ }
3384
+ function syncedBackingRevokeStatements({
3385
+ backings,
3386
+ executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
3387
+ }) {
3388
+ const owner = quoteIdentifier(executionOwnerRole);
3389
+ const seen = /* @__PURE__ */ new Set();
3390
+ const statements = [];
3391
+ for (const backing of backings) {
3392
+ const physical = splitPhysicalRelation(backing.physicalRelation);
3393
+ const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
3394
+ if (seen.has(physicalTarget)) continue;
3395
+ seen.add(physicalTarget);
3396
+ statements.push(`REVOKE SELECT ON ${physicalTarget} FROM ${owner}`);
3397
+ }
3398
+ return statements;
3399
+ }
3400
+ var FACTORIES = {
3401
+ uuid: (name) => uuid(name),
3402
+ text: (name) => text(name),
3403
+ integer: (name) => integer(name),
3404
+ // Mode 'bigint' keeps int8 lossless — mode 'number' silently rounds past
3405
+ // 2^53. The read executor serializes BigInt to string for JSON.
3406
+ bigint: (name) => bigint(name, { mode: "bigint" }),
3407
+ numeric: (name) => numeric(name),
3408
+ double: (name) => doublePrecision(name),
3409
+ boolean: (name) => boolean(name),
3410
+ timestamptz: (name) => timestamp(name, { withTimezone: true }),
3411
+ date: (name) => date(name),
3412
+ jsonb: (name) => jsonb(name)
3413
+ };
3414
+ function buildColumn(name, definition) {
3415
+ let column = FACTORIES[definition.type](name);
3416
+ if (definition.primary_key) column = column.primaryKey();
3417
+ if (definition.nullable === false && !definition.primary_key) column = column.notNull();
3418
+ if (definition.unique && !definition.primary_key) column = column.unique();
3419
+ if (definition.default !== void 0) {
3420
+ const value = definition.default;
3421
+ column = column.default(
3422
+ typeof value === "string" && isFunctionDefault(value, definition.type) ? sql.raw(value) : value
3423
+ );
3424
+ }
3425
+ return column;
3426
+ }
3427
+
3428
+ // src/engine/tables.ts
3429
+ var SCHEMA_IMPORT_KEY = "$schema";
3430
+ function buildDrizzleEntities(definition) {
3431
+ const schemaName = ormSchemaName(definition.namespace);
3432
+ const schema = pgSchema(schemaName);
3433
+ const imports = { [SCHEMA_IMPORT_KEY]: schema };
3434
+ const tables = {};
3435
+ const built = /* @__PURE__ */ new Map();
3436
+ for (const table of sortByEnforcedLinks(columnTables(definition))) {
3437
+ const drizzleTable = buildTable(schema, table, built);
3438
+ built.set(table.table, drizzleTable);
3439
+ imports[table.table] = drizzleTable;
3440
+ tables[table.table] = drizzleTable;
3441
+ }
3442
+ return { imports, tables, schemaName };
3443
+ }
3444
+ function buildTable(schema, table, built) {
3445
+ const columns = Object.fromEntries(
3446
+ Object.entries(table.columns ?? {}).map(([name, column]) => [name, buildColumn(name, column)])
3447
+ );
3448
+ const drizzleTable = schema.table(table.table, columns, (self) => [
3449
+ ...buildIndexes(table, self),
3450
+ ...buildForeignKeys(table, self, built)
3451
+ ]);
3452
+ return drizzleTable.enableRLS();
3453
+ }
3454
+ function buildIndexes(table, self) {
3455
+ return (table.indexes ?? []).map((definition) => {
3456
+ const name = indexName(table.table, definition);
3457
+ const factory = definition.unique ? uniqueIndex(name) : index(name);
3458
+ const columns = definition.columns.map((column) => self[column]);
3459
+ return factory.on(...columns);
3460
+ });
3461
+ }
3462
+ function buildForeignKeys(table, self, built) {
3463
+ return Object.entries(table.links ?? {}).filter(([, link]) => link.enforce).map(([name, link]) => {
3464
+ const target = link.to === table.table ? self : built.get(link.to);
3465
+ const targetColumn = target && findPrimaryKeyColumn(target);
3466
+ if (!target || !targetColumn) {
3467
+ throw new OrmDefinitionError(`enforced link '${name}' on '${table.table}' has no built target '${link.to}'`);
3468
+ }
3469
+ return foreignKey({
3470
+ name: foreignKeyName(table.table, name),
3471
+ columns: [self[link.on]],
3472
+ foreignColumns: [targetColumn]
3473
+ });
3474
+ });
3475
+ }
3476
+ function findPrimaryKeyColumn(target) {
3477
+ const columns = Object.values(target).filter(
3478
+ (value) => value !== null && typeof value === "object" && "primary" in value
3479
+ );
3480
+ return columns.find((column) => column.primary);
3481
+ }
3482
+ function sortByEnforcedLinks(tables) {
3483
+ const remaining = new Map(tables.map((table) => [table.table, table]));
3484
+ const sorted = [];
3485
+ const placed = /* @__PURE__ */ new Set();
3486
+ while (remaining.size > 0) {
3487
+ const ready = [...remaining.values()].filter(
3488
+ (table) => enforcedTargets(table).every((target) => placed.has(target) || !remaining.has(target))
3489
+ );
3490
+ if (ready.length === 0) {
3491
+ throw new OrmDefinitionError(`enforced links form a cycle among: ${[...remaining.keys()].join(", ")}`);
3492
+ }
3493
+ for (const table of ready) {
3494
+ sorted.push(table);
3495
+ placed.add(table.table);
3496
+ remaining.delete(table.table);
3497
+ }
3498
+ }
3499
+ return sorted;
3500
+ }
3501
+ var enforcedTargets = (table) => Object.values(table.links ?? {}).filter((link) => link.enforce && !isQualifiedReference(link.to) && link.to !== table.table).map((link) => link.to);
3502
+
3503
+ // src/engine/snapshot.ts
3504
+ function generateSnapshot(definition, previousId3) {
3505
+ const { imports, schemaName } = buildDrizzleEntities(definition);
3506
+ return generateDrizzleJson(imports, previousId3, [schemaName]);
3507
+ }
3508
+ var emptySnapshot = (namespace) => generateDrizzleJson({}, void 0, [ormSchemaName(namespace)]);
3509
+ var CURRENT_SNAPSHOT_VERSION = generateDrizzleJson({}).version;
3510
+ function normalizeSnapshot(snapshot) {
3511
+ const upgraded = upPgSnapshot(snapshot);
3512
+ const version = upgraded.version;
3513
+ if (version !== CURRENT_SNAPSHOT_VERSION) {
3514
+ throw new OrmDefinitionError(
3515
+ `snapshot format '${String(version)}' is not supported by this drizzle-kit (expected '${CURRENT_SNAPSHOT_VERSION}') \u2014 upgrade @sequenceholdings/orm`
3516
+ );
3517
+ }
3518
+ return upgraded;
3519
+ }
3520
+ function diffStatements(previous, current) {
3521
+ assertUnambiguousDiff(previous, current);
3522
+ return generateMigration(previous, current);
3523
+ }
3524
+ function assertUnambiguousDiff(previous, current) {
3525
+ const previousTables = tablesOf(previous);
3526
+ const currentTables = tablesOf(current);
3527
+ const removedTables = [...previousTables.keys()].filter((name) => !currentTables.has(name));
3528
+ const addedTables = [...currentTables.keys()].filter((name) => !previousTables.has(name));
3529
+ if (removedTables.length > 0 && addedTables.length > 0) {
3530
+ throw new OrmDefinitionError(
3531
+ `ambiguous diff: table(s) added (${addedTables.join(", ")}) and removed (${removedTables.join(", ")}) in the same change \u2014 ship the add and the drop as separate changes`
3532
+ );
3533
+ }
3534
+ for (const [name, previousColumns] of previousTables) {
3535
+ const currentColumns = currentTables.get(name);
3536
+ if (!currentColumns) continue;
3537
+ const removed = [...previousColumns].filter((column) => !currentColumns.has(column));
3538
+ const added = [...currentColumns].filter((column) => !previousColumns.has(column));
3539
+ if (removed.length > 0 && added.length > 0) {
3540
+ throw new OrmDefinitionError(
3541
+ `ambiguous diff on '${name}': column(s) added (${added.join(", ")}) and removed (${removed.join(", ")}) in the same change \u2014 declare \`renamed_from\` for a rename, or ship the add and the drop as separate changes`
3542
+ );
3543
+ }
3544
+ }
3545
+ }
3546
+ function tablesOf(snapshot) {
3547
+ const tables = snapshot.tables ?? {};
3548
+ return new Map(
3549
+ Object.values(tables).map((table) => [`${table.schema}.${table.name}`, new Set(Object.keys(table.columns))])
3550
+ );
3551
+ }
3552
+ function applyMigrationHints(previous, definition) {
3553
+ const adjusted = structuredClone(previous);
3554
+ const statements = [];
3555
+ const schemaName = ormSchemaName(definition.namespace);
3556
+ const qualified = (table) => `${quoteIdentifier(schemaName)}.${quoteIdentifier(table)}`;
3557
+ for (const table of columnTables(definition)) {
3558
+ const snapshotTable = findTable(adjusted, schemaName, table.table);
3559
+ if (!snapshotTable) continue;
3560
+ for (const [name, column] of Object.entries(table.columns ?? {})) {
3561
+ if (column.renamed_from) {
3562
+ const previousColumn = snapshotTable.columns[column.renamed_from];
3563
+ if (previousColumn && snapshotTable.columns[name]) {
3564
+ throw new OrmDefinitionError(
3565
+ `'${table.table}.${name}' declares renamed_from '${column.renamed_from}', but both columns exist in the previous schema \u2014 a rename cannot overwrite a live column`
3566
+ );
3567
+ }
3568
+ if (previousColumn) {
3569
+ statements.push(
3570
+ `ALTER TABLE ${qualified(table.table)} RENAME COLUMN ${quoteIdentifier(column.renamed_from)} TO ${quoteIdentifier(name)}`
3571
+ );
3572
+ delete snapshotTable.columns[column.renamed_from];
3573
+ snapshotTable.columns[name] = { ...previousColumn, name };
3574
+ } else if (!snapshotTable.columns[name]) {
3575
+ throw new OrmDefinitionError(
3576
+ `'${table.table}.${name}' declares renamed_from '${column.renamed_from}', but the previous schema has neither column \u2014 check the declared source name`
3577
+ );
3578
+ }
3579
+ }
3580
+ const cast = column.cast ? castParts(column.cast) : null;
3581
+ if (cast) {
3582
+ const current = snapshotTable.columns[name];
3583
+ const targetType = sqlTypeName(column.type);
3584
+ if (current && current.type !== targetType) {
3585
+ statements.push(
3586
+ `ALTER TABLE ${qualified(table.table)} ALTER COLUMN ${quoteIdentifier(name)} TYPE ${targetType} USING ${quoteIdentifier(name)}::${targetType}`
3587
+ );
3588
+ current.type = targetType;
3589
+ }
3590
+ }
3591
+ }
3592
+ }
3593
+ return { statements, adjusted };
1146
3594
  }
1147
- var columnTables = (definition) => definition.tables.filter((table) => !table.source);
1148
- var FACTORIES = {
3595
+ function findTable(snapshot, schema, table) {
3596
+ const tables = snapshot.tables ?? {};
3597
+ return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
3598
+ }
3599
+ function snapshotTableNames(snapshot, schema) {
3600
+ const tables = snapshot.tables ?? {};
3601
+ return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
3602
+ }
3603
+ var FACTORIES2 = {
1149
3604
  uuid: (name) => uuid(name),
1150
3605
  text: (name) => text(name),
1151
3606
  integer: (name) => integer(name),
@@ -1159,81 +3614,418 @@ var FACTORIES = {
1159
3614
  date: (name) => date(name),
1160
3615
  jsonb: (name) => jsonb(name)
1161
3616
  };
1162
- function buildColumn(name, definition) {
1163
- let column = FACTORIES[definition.type](name);
1164
- if (definition.primary_key) column = column.primaryKey();
1165
- if (definition.nullable === false && !definition.primary_key) column = column.notNull();
1166
- if (definition.unique && !definition.primary_key) column = column.unique();
1167
- if (definition.default !== void 0) {
1168
- const value = definition.default;
1169
- column = column.default(
1170
- typeof value === "string" && isFunctionDefault(value, definition.type) ? sql.raw(value) : value
1171
- );
1172
- }
1173
- return column;
1174
- }
1175
-
1176
- // src/engine/tables.ts
1177
- var SCHEMA_IMPORT_KEY = "$schema";
1178
- function buildDrizzleEntities(definition) {
3617
+ var SCHEMA_IMPORT_KEY2 = "$schema";
3618
+ var enumImportKey = (name) => `$enum_${name}`;
3619
+ var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
3620
+ function buildDrizzleEntitiesV2(definition) {
1179
3621
  const schemaName = ormSchemaName(definition.namespace);
1180
3622
  const schema = pgSchema(schemaName);
1181
- const imports = { [SCHEMA_IMPORT_KEY]: schema };
3623
+ const imports = { [SCHEMA_IMPORT_KEY2]: schema };
1182
3624
  const tables = {};
1183
3625
  const built = /* @__PURE__ */ new Map();
1184
- for (const table of sortByEnforcedLinks(columnTables(definition))) {
1185
- const drizzleTable = buildTable(schema, table, built);
3626
+ const enums = /* @__PURE__ */ new Map();
3627
+ for (const enumDefinition of definition.enums) {
3628
+ const values = enumDefinition.values;
3629
+ const builtEnum = schema.enum(enumDefinition.name, values);
3630
+ enums.set(enumDefinition.name, builtEnum);
3631
+ imports[enumImportKey(enumDefinition.name)] = builtEnum;
3632
+ }
3633
+ const byName = new Map(definition.tables.map((table) => [table.table, table]));
3634
+ for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
3635
+ const drizzleTable = buildTable2(schema, table, built, enums, byName);
1186
3636
  built.set(table.table, drizzleTable);
1187
3637
  imports[table.table] = drizzleTable;
1188
3638
  tables[table.table] = drizzleTable;
1189
3639
  }
3640
+ for (const view of definition.views ?? []) {
3641
+ const columns = Object.fromEntries(
3642
+ view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
3643
+ );
3644
+ imports[view.view] = schema.view(view.view, columns).as(
3645
+ sql.raw(
3646
+ rewriteSyncedSourceRelationsInViewQuery({
3647
+ query: view.query,
3648
+ canonical: definition,
3649
+ schemaName,
3650
+ dependencies: view.dependencies
3651
+ })
3652
+ )
3653
+ );
3654
+ }
1190
3655
  return { imports, tables, schemaName };
1191
3656
  }
1192
- function buildTable(schema, table, built) {
3657
+ function rewriteSyncedSourceRelationsInViewQuery({
3658
+ query,
3659
+ canonical,
3660
+ schemaName,
3661
+ dependencies
3662
+ }) {
3663
+ const tablesBySource = /* @__PURE__ */ new Map();
3664
+ for (const table of canonical.tables) {
3665
+ if (!table.source) continue;
3666
+ tablesBySource.set(table.source, [...tablesBySource.get(table.source) ?? [], table.table]);
3667
+ }
3668
+ const declared = new Set(dependencies ?? []);
3669
+ const boundTableBySource = /* @__PURE__ */ new Map();
3670
+ for (const [source, tables] of tablesBySource) {
3671
+ if (tables.length === 1) {
3672
+ boundTableBySource.set(source, tables[0]);
3673
+ continue;
3674
+ }
3675
+ const [sourceSchema, sourceName] = source.split(".");
3676
+ if (!sourceSchema || !sourceName) continue;
3677
+ const referenced = sqlReferencesQualifiedRelation(query, sourceSchema, sourceName);
3678
+ if (!referenced) continue;
3679
+ const declaredMatches = tables.filter((table) => declared.has(table));
3680
+ if (declaredMatches.length === 1) {
3681
+ boundTableBySource.set(source, declaredMatches[0]);
3682
+ continue;
3683
+ }
3684
+ throw new OrmDefinitionError(
3685
+ `synced source '${source}' is declared by multiple synced tables (${[...tables].sort().join(", ")}) \u2014 this view references it but its declared dependencies do not name exactly one of them, so the rewrite is ambiguous`
3686
+ );
3687
+ }
3688
+ return transformSqlOutsideLiterals(query, (segment) => {
3689
+ let rewritten = segment;
3690
+ for (const table of canonical.tables) {
3691
+ if (!table.source) continue;
3692
+ const boundTable = boundTableBySource.get(table.source);
3693
+ if (boundTable === void 0 || boundTable !== table.table) continue;
3694
+ const [sourceSchema, sourceName] = table.source.split(".");
3695
+ if (!sourceSchema || !sourceName) continue;
3696
+ rewritten = replaceQualifiedRelation(
3697
+ rewritten,
3698
+ sourceSchema,
3699
+ sourceName,
3700
+ schemaName,
3701
+ table.table
3702
+ );
3703
+ }
3704
+ return rewritten;
3705
+ });
3706
+ }
3707
+ function transformSqlOutsideLiterals(sql4, transform) {
3708
+ const out = [];
3709
+ let state = { kind: "normal" };
3710
+ let i = 0;
3711
+ let codeStart = 0;
3712
+ const emitCode = (end) => {
3713
+ if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
3714
+ codeStart = end;
3715
+ };
3716
+ while (i < sql4.length) {
3717
+ const c = sql4[i];
3718
+ const next = i + 1 < sql4.length ? sql4[i + 1] : "";
3719
+ switch (state.kind) {
3720
+ case "normal": {
3721
+ if (c === "-" && next === "-") {
3722
+ emitCode(i);
3723
+ state = { kind: "line-comment" };
3724
+ out.push("--");
3725
+ i += 2;
3726
+ codeStart = i;
3727
+ break;
3728
+ }
3729
+ if (c === "/" && next === "*") {
3730
+ emitCode(i);
3731
+ state = { kind: "block-comment", depth: 1 };
3732
+ out.push("/*");
3733
+ i += 2;
3734
+ codeStart = i;
3735
+ break;
3736
+ }
3737
+ if ((c === "E" || c === "e") && next === "'") {
3738
+ const prev = i > 0 ? sql4[i - 1] : "";
3739
+ if (!/[A-Za-z0-9_]/.test(prev)) {
3740
+ emitCode(i);
3741
+ state = { kind: "e-single" };
3742
+ out.push("E'");
3743
+ i += 2;
3744
+ codeStart = i;
3745
+ break;
3746
+ }
3747
+ }
3748
+ if (c === "'") {
3749
+ emitCode(i);
3750
+ state = { kind: "single" };
3751
+ out.push("'");
3752
+ i += 1;
3753
+ codeStart = i;
3754
+ break;
3755
+ }
3756
+ if (c === "$") {
3757
+ const prev = i > 0 ? sql4[i - 1] : "";
3758
+ if (!/[A-Za-z0-9_$]/.test(prev)) {
3759
+ const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
3760
+ if (tagMatch) {
3761
+ emitCode(i);
3762
+ state = { kind: "dollar", tag: tagMatch[1] ?? "" };
3763
+ out.push(tagMatch[0]);
3764
+ i += tagMatch[0].length;
3765
+ codeStart = i;
3766
+ break;
3767
+ }
3768
+ }
3769
+ }
3770
+ i += 1;
3771
+ break;
3772
+ }
3773
+ case "single": {
3774
+ if (c === "'" && next === "'") {
3775
+ out.push("''");
3776
+ i += 2;
3777
+ break;
3778
+ }
3779
+ if (c === "'") {
3780
+ out.push("'");
3781
+ state = { kind: "normal" };
3782
+ i += 1;
3783
+ codeStart = i;
3784
+ break;
3785
+ }
3786
+ out.push(c);
3787
+ i += 1;
3788
+ break;
3789
+ }
3790
+ case "e-single": {
3791
+ if (c === "\\" && i + 1 < sql4.length) {
3792
+ out.push(c, sql4[i + 1]);
3793
+ i += 2;
3794
+ break;
3795
+ }
3796
+ if (c === "'" && next === "'") {
3797
+ out.push("''");
3798
+ i += 2;
3799
+ break;
3800
+ }
3801
+ if (c === "'") {
3802
+ out.push("'");
3803
+ state = { kind: "normal" };
3804
+ i += 1;
3805
+ codeStart = i;
3806
+ break;
3807
+ }
3808
+ out.push(c);
3809
+ i += 1;
3810
+ break;
3811
+ }
3812
+ case "dollar": {
3813
+ const closer = `$${state.tag}$`;
3814
+ if (sql4.startsWith(closer, i)) {
3815
+ out.push(closer);
3816
+ state = { kind: "normal" };
3817
+ i += closer.length;
3818
+ codeStart = i;
3819
+ break;
3820
+ }
3821
+ out.push(c);
3822
+ i += 1;
3823
+ break;
3824
+ }
3825
+ case "line-comment": {
3826
+ if (c === "\n") {
3827
+ out.push("\n");
3828
+ state = { kind: "normal" };
3829
+ i += 1;
3830
+ codeStart = i;
3831
+ break;
3832
+ }
3833
+ out.push(c);
3834
+ i += 1;
3835
+ break;
3836
+ }
3837
+ case "block-comment": {
3838
+ if (c === "/" && next === "*") {
3839
+ out.push("/*");
3840
+ state = { kind: "block-comment", depth: state.depth + 1 };
3841
+ i += 2;
3842
+ break;
3843
+ }
3844
+ if (c === "*" && next === "/") {
3845
+ out.push("*/");
3846
+ const newDepth = state.depth - 1;
3847
+ state = newDepth === 0 ? { kind: "normal" } : { kind: "block-comment", depth: newDepth };
3848
+ i += 2;
3849
+ if (state.kind === "normal") codeStart = i;
3850
+ break;
3851
+ }
3852
+ out.push(c);
3853
+ i += 1;
3854
+ break;
3855
+ }
3856
+ }
3857
+ }
3858
+ emitCode(sql4.length);
3859
+ return out.join("");
3860
+ }
3861
+ function escapeRegExp(value) {
3862
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3863
+ }
3864
+ function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
3865
+ let referenced = false;
3866
+ transformSqlOutsideLiterals(sql4, (segment) => {
3867
+ if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
3868
+ return segment;
3869
+ });
3870
+ return referenced;
3871
+ }
3872
+ function qualifiedRelationPattern(sourceSchema, sourceName) {
3873
+ const schemaPattern = `(?:"${escapeRegExp(sourceSchema)}"|${escapeRegExp(sourceSchema)})`;
3874
+ const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
3875
+ return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
3876
+ }
3877
+ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
3878
+ return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
3879
+ }
3880
+ function generateSnapshotV2(definition, previousId3) {
3881
+ const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
3882
+ const snapshot = generateDrizzleJson(imports, previousId3, [schemaName]);
3883
+ if ((definition.views?.length ?? 0) > 0) {
3884
+ const extended = snapshot;
3885
+ extended._sequenceOrm = {
3886
+ viewDependencies: Object.fromEntries(
3887
+ (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
3888
+ )
3889
+ };
3890
+ }
3891
+ return snapshot;
3892
+ }
3893
+ function buildViewColumnV2({
3894
+ view,
3895
+ column,
3896
+ enums
3897
+ }) {
3898
+ const factory = resolveColumnFactory(view, column, enums);
3899
+ let builder = factory(column.columnName);
3900
+ if (!column.nullable) builder = builder.notNull();
3901
+ return builder;
3902
+ }
3903
+ function buildTable2(schema, table, built, enums, byName) {
3904
+ const primaryKeyColumns = table.primaryKey ?? [];
3905
+ if (primaryKeyColumns.length === 0) {
3906
+ throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
3907
+ }
3908
+ const columnNames = new Set(table.columns.map((column) => column.columnName));
3909
+ for (const name of primaryKeyColumns) {
3910
+ if (!columnNames.has(name)) {
3911
+ throw new OrmDefinitionError(`primaryKey of '${table.table}' references unknown column '${name}'`);
3912
+ }
3913
+ }
3914
+ const singlePrimaryKey = primaryKeyColumns.length === 1 ? primaryKeyColumns[0] : void 0;
3915
+ const compositeMembers = new Set(singlePrimaryKey === void 0 ? primaryKeyColumns : []);
1193
3916
  const columns = Object.fromEntries(
1194
- Object.entries(table.columns ?? {}).map(([name, column]) => [name, buildColumn(name, column)])
3917
+ table.columns.map((column) => [
3918
+ column.columnName,
3919
+ buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
3920
+ ])
1195
3921
  );
1196
3922
  const drizzleTable = schema.table(table.table, columns, (self) => [
1197
- ...buildIndexes(table, self),
1198
- ...buildForeignKeys(table, self, built)
3923
+ ...buildCompositePrimaryKey(table, primaryKeyColumns, self),
3924
+ ...buildIndexes2(table, self),
3925
+ ...buildForeignKeys2(table, self, built, byName)
1199
3926
  ]);
1200
3927
  return drizzleTable.enableRLS();
1201
3928
  }
1202
- function buildIndexes(table, self) {
1203
- return (table.indexes ?? []).map((definition) => {
3929
+ function resolveColumnFactory(table, column, enums) {
3930
+ if (typeof column.type === "string") return FACTORIES2[column.type];
3931
+ const enumFactory = enums.get(column.type.enum);
3932
+ if (!enumFactory) {
3933
+ throw new OrmDefinitionError(`column '${table}.${column.columnName}' references undeclared enum '${column.type.enum}'`);
3934
+ }
3935
+ return enumFactory;
3936
+ }
3937
+ function buildColumnV2({
3938
+ table,
3939
+ column,
3940
+ singlePrimaryKey,
3941
+ compositeMembers,
3942
+ enums
3943
+ }) {
3944
+ const factory = resolveColumnFactory(table, column, enums);
3945
+ let builder = factory(column.columnName);
3946
+ const isSinglePrimaryKey = column.columnName === singlePrimaryKey;
3947
+ if (isSinglePrimaryKey) builder = builder.primaryKey();
3948
+ if ((column.nullable === false || compositeMembers.has(column.columnName)) && !isSinglePrimaryKey) {
3949
+ builder = builder.notNull();
3950
+ }
3951
+ if (column.unique && !isSinglePrimaryKey) builder = builder.unique();
3952
+ if (column.defaultFn !== void 0 && column.default !== void 0) {
3953
+ throw new OrmDefinitionError(`column '${table}.${column.columnName}' declares both default and defaultFn`);
3954
+ }
3955
+ if (column.defaultFn !== void 0) {
3956
+ if (typeof column.type !== "string" || !isFunctionDefault(column.defaultFn, column.type)) {
3957
+ throw new OrmDefinitionError(
3958
+ `column '${table}.${column.columnName}' declares defaultFn '${column.defaultFn}' which is invalid for its type`
3959
+ );
3960
+ }
3961
+ builder = builder.default(sql.raw(column.defaultFn));
3962
+ } else if (column.default !== void 0 && column.default !== null) {
3963
+ builder = builder.default(column.default);
3964
+ }
3965
+ return builder;
3966
+ }
3967
+ function buildCompositePrimaryKey(table, primaryKeyColumns, self) {
3968
+ if (primaryKeyColumns.length < 2) return [];
3969
+ return [
3970
+ primaryKey({
3971
+ name: `${table.table}_pkey`,
3972
+ columns: primaryKeyColumns.map((column) => self[column])
3973
+ })
3974
+ ];
3975
+ }
3976
+ function buildIndexes2(table, self) {
3977
+ return table.indexes.map((definition) => {
3978
+ for (const column of definition.columns) {
3979
+ if (!(column in self)) {
3980
+ throw new OrmDefinitionError(`index on '${table.table}' references unknown column '${column}'`);
3981
+ }
3982
+ }
1204
3983
  const name = indexName(table.table, definition);
1205
3984
  const factory = definition.unique ? uniqueIndex(name) : index(name);
1206
3985
  const columns = definition.columns.map((column) => self[column]);
1207
3986
  return factory.on(...columns);
1208
3987
  });
1209
3988
  }
1210
- function buildForeignKeys(table, self, built) {
1211
- return Object.entries(table.links ?? {}).filter(([, link]) => link.enforce).map(([name, link]) => {
1212
- const target = link.to === table.table ? self : built.get(link.to);
1213
- const targetColumn = target && findPrimaryKeyColumn(target);
1214
- if (!target || !targetColumn) {
1215
- throw new OrmDefinitionError(`enforced link '${name}' on '${table.table}' has no built target '${link.to}'`);
3989
+ function buildForeignKeys2(table, self, built, byName) {
3990
+ return table.links.filter((link) => link.enforce).map((link) => {
3991
+ if (link.kind !== "toOne" || !link.target) {
3992
+ throw new OrmDefinitionError(
3993
+ `enforced link '${link.name}' on '${table.table}' must be a toOne link with a namespace-local target`
3994
+ );
3995
+ }
3996
+ const targetTable = byName.get(link.target);
3997
+ const target = link.target === table.table ? self : built.get(link.target);
3998
+ if (!targetTable || !target) {
3999
+ throw new OrmDefinitionError(`enforced link '${link.name}' on '${table.table}' has no built target '${link.target}'`);
4000
+ }
4001
+ const referencedColumn = link.references ?? singlePrimaryKeyOf(targetTable);
4002
+ if (!referencedColumn) {
4003
+ throw new OrmDefinitionError(
4004
+ `enforced link '${link.name}' on '${table.table}' targets '${link.target}' whose primary key is composite \u2014 declare 'references'`
4005
+ );
4006
+ }
4007
+ const targetColumn = target[referencedColumn];
4008
+ const sourceColumn = self[link.on];
4009
+ if (!targetColumn || !sourceColumn) {
4010
+ throw new OrmDefinitionError(
4011
+ `enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
4012
+ );
1216
4013
  }
1217
4014
  return foreignKey({
1218
- name: foreignKeyName(table.table, name),
1219
- columns: [self[link.on]],
4015
+ name: foreignKeyName(table.table, link.name),
4016
+ columns: [sourceColumn],
1220
4017
  foreignColumns: [targetColumn]
1221
4018
  });
1222
4019
  });
1223
4020
  }
1224
- function findPrimaryKeyColumn(target) {
1225
- const columns = Object.values(target).filter(
1226
- (value) => value !== null && typeof value === "object" && "primary" in value
1227
- );
1228
- return columns.find((column) => column.primary);
1229
- }
1230
- function sortByEnforcedLinks(tables) {
4021
+ var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
4022
+ function sortByEnforcedLinks2(tables) {
1231
4023
  const remaining = new Map(tables.map((table) => [table.table, table]));
1232
4024
  const sorted = [];
1233
4025
  const placed = /* @__PURE__ */ new Set();
1234
4026
  while (remaining.size > 0) {
1235
4027
  const ready = [...remaining.values()].filter(
1236
- (table) => enforcedTargets(table).every((target) => placed.has(target) || !remaining.has(target))
4028
+ (table) => enforcedTargets2(table).every((target) => placed.has(target) || !remaining.has(target))
1237
4029
  );
1238
4030
  if (ready.length === 0) {
1239
4031
  throw new OrmDefinitionError(`enforced links form a cycle among: ${[...remaining.keys()].join(", ")}`);
@@ -1246,108 +4038,376 @@ function sortByEnforcedLinks(tables) {
1246
4038
  }
1247
4039
  return sorted;
1248
4040
  }
1249
- var enforcedTargets = (table) => Object.values(table.links ?? {}).filter((link) => link.enforce && !isQualifiedReference(link.to) && link.to !== table.table).map((link) => link.to);
1250
-
1251
- // src/engine/snapshot.ts
1252
- function generateSnapshot(definition, previousId2) {
1253
- const { imports, schemaName } = buildDrizzleEntities(definition);
1254
- return generateDrizzleJson(imports, previousId2, [schemaName]);
4041
+ var enforcedTargets2 = (table) => table.links.flatMap(
4042
+ (link) => (
4043
+ // A self-target is not an ordering dependency — the FK builds inline.
4044
+ link.enforce && link.target !== void 0 && link.target !== table.table ? [link.target] : []
4045
+ )
4046
+ );
4047
+ function applyMigrationHintsV2(previous, definition) {
4048
+ const adjusted = structuredClone(previous);
4049
+ const statements = [];
4050
+ const schemaName = ormSchemaName(definition.namespace);
4051
+ const qualified = (table) => `${quoteIdentifier(schemaName)}.${quoteIdentifier(table)}`;
4052
+ const enumsByName = new Map(definition.enums.map((entry) => [entry.name, entry]));
4053
+ for (const table of columnTablesV2(definition)) {
4054
+ if (table.renamedFrom) {
4055
+ applyTableRename({ adjusted, schemaName, table, renamedFrom: table.renamedFrom, statements, qualified });
4056
+ }
4057
+ const snapshotTable = findTable2(adjusted, schemaName, table.table);
4058
+ if (!snapshotTable) continue;
4059
+ for (const column of table.columns) {
4060
+ const name = column.columnName;
4061
+ if (column.renamedFrom) {
4062
+ const previousColumn = snapshotTable.columns[column.renamedFrom];
4063
+ if (previousColumn && snapshotTable.columns[name]) {
4064
+ throw new OrmDefinitionError(
4065
+ `'${table.table}.${name}' declares renamedFrom '${column.renamedFrom}', but both columns exist in the previous schema \u2014 a rename cannot overwrite a live column`
4066
+ );
4067
+ }
4068
+ if (previousColumn) {
4069
+ statements.push(
4070
+ `ALTER TABLE ${qualified(table.table)} RENAME COLUMN ${quoteIdentifier(column.renamedFrom)} TO ${quoteIdentifier(name)}`
4071
+ );
4072
+ delete snapshotTable.columns[column.renamedFrom];
4073
+ snapshotTable.columns[name] = { ...previousColumn, name };
4074
+ } else if (!snapshotTable.columns[name]) {
4075
+ throw new OrmDefinitionError(
4076
+ `'${table.table}.${name}' declares renamedFrom '${column.renamedFrom}', but the previous schema has neither column \u2014 check the declared source name`
4077
+ );
4078
+ }
4079
+ }
4080
+ const cast = column.cast ? castParts(column.cast) : null;
4081
+ if (cast) {
4082
+ const current = snapshotTable.columns[name];
4083
+ const { snapshotType, sqlType } = castTargetTypes(column, enumsByName, schemaName, table.table);
4084
+ if (current && current.type !== snapshotType) {
4085
+ statements.push(
4086
+ `ALTER TABLE ${qualified(table.table)} ALTER COLUMN ${quoteIdentifier(name)} TYPE ${sqlType} USING ${quoteIdentifier(name)}::${sqlType}`
4087
+ );
4088
+ current.type = snapshotType;
4089
+ }
4090
+ }
4091
+ }
4092
+ }
4093
+ return { statements, adjusted };
1255
4094
  }
1256
- var emptySnapshot = (namespace) => generateDrizzleJson({}, void 0, [ormSchemaName(namespace)]);
1257
- var CURRENT_SNAPSHOT_VERSION = generateDrizzleJson({}).version;
1258
- function normalizeSnapshot(snapshot) {
1259
- const upgraded = upPgSnapshot(snapshot);
1260
- const version = upgraded.version;
1261
- if (version !== CURRENT_SNAPSHOT_VERSION) {
4095
+ function castTargetTypes(column, enums, schemaName, table) {
4096
+ if (typeof column.type === "string") {
4097
+ const name = sqlTypeName(column.type);
4098
+ return { snapshotType: name, sqlType: name };
4099
+ }
4100
+ const enumName = column.type.enum;
4101
+ if (!enums.has(enumName)) {
4102
+ throw new OrmDefinitionError(`column '${table}.${column.columnName}' casts to undeclared enum '${enumName}'`);
4103
+ }
4104
+ return { snapshotType: enumName, sqlType: `${quoteIdentifier(schemaName)}.${quoteIdentifier(enumName)}` };
4105
+ }
4106
+ function applyTableRename({
4107
+ adjusted,
4108
+ schemaName,
4109
+ table,
4110
+ renamedFrom,
4111
+ statements,
4112
+ qualified
4113
+ }) {
4114
+ const tables = adjusted.tables ?? {};
4115
+ const previousEntry = Object.entries(tables).find(
4116
+ ([, entry]) => entry.schema === schemaName && entry.name === renamedFrom
4117
+ );
4118
+ const currentTable = findTable2(adjusted, schemaName, table.table);
4119
+ if (previousEntry && currentTable) {
1262
4120
  throw new OrmDefinitionError(
1263
- `snapshot format '${String(version)}' is not supported by this drizzle-kit (expected '${CURRENT_SNAPSHOT_VERSION}') \u2014 upgrade @sequenceholdings/orm`
4121
+ `'${table.table}' declares renamedFrom '${renamedFrom}', but both tables exist in the previous schema \u2014 a rename cannot overwrite a live table`
1264
4122
  );
1265
4123
  }
1266
- return upgraded;
1267
- }
1268
- function diffStatements(previous, current) {
1269
- assertUnambiguousDiff(previous, current);
1270
- return generateMigration(previous, current);
1271
- }
1272
- function assertUnambiguousDiff(previous, current) {
1273
- const previousTables = tablesOf(previous);
1274
- const currentTables = tablesOf(current);
1275
- const removedTables = [...previousTables.keys()].filter((name) => !currentTables.has(name));
1276
- const addedTables = [...currentTables.keys()].filter((name) => !previousTables.has(name));
1277
- if (removedTables.length > 0 && addedTables.length > 0) {
4124
+ if (!previousEntry) {
4125
+ if (currentTable) return;
1278
4126
  throw new OrmDefinitionError(
1279
- `ambiguous diff: table(s) added (${addedTables.join(", ")}) and removed (${removedTables.join(", ")}) in the same change \u2014 ship the add and the drop as separate changes`
4127
+ `'${table.table}' declares renamedFrom '${renamedFrom}', but the previous schema has neither table \u2014 check the declared source name`
1280
4128
  );
1281
4129
  }
1282
- for (const [name, previousColumns] of previousTables) {
1283
- const currentColumns = currentTables.get(name);
1284
- if (!currentColumns) continue;
1285
- const removed = [...previousColumns].filter((column) => !currentColumns.has(column));
1286
- const added = [...currentColumns].filter((column) => !previousColumns.has(column));
1287
- if (removed.length > 0 && added.length > 0) {
1288
- throw new OrmDefinitionError(
1289
- `ambiguous diff on '${name}': column(s) added (${added.join(", ")}) and removed (${removed.join(", ")}) in the same change \u2014 declare \`renamed_from\` for a rename, or ship the add and the drop as separate changes`
1290
- );
4130
+ statements.push(`ALTER TABLE ${qualified(renamedFrom)} RENAME TO ${quoteIdentifier(table.table)}`);
4131
+ const [previousKey, previousTable] = previousEntry;
4132
+ delete tables[previousKey];
4133
+ tables[`${schemaName}.${table.table}`] = { ...previousTable, name: table.table };
4134
+ }
4135
+ function findTable2(snapshot, schema, table) {
4136
+ const tables = snapshot.tables ?? {};
4137
+ return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
4138
+ }
4139
+ async function planMigrationV2({
4140
+ canonical,
4141
+ previousSnapshot
4142
+ }) {
4143
+ const schema = ormSchemaName(canonical.namespace);
4144
+ const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
4145
+ assertValidViewDependencies(canonical);
4146
+ const hints = applyMigrationHintsV2(previous, canonical);
4147
+ const snapshot = generateSnapshotV2(canonical, previousId(previous));
4148
+ const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
4149
+ const previousViews = snapshotViews(hints.adjusted);
4150
+ const currentViews = snapshotViews(snapshot);
4151
+ const previousDependencies = snapshotViewDependencies(hints.adjusted);
4152
+ const currentDependencies = snapshotViewDependencies(snapshot);
4153
+ const affected = affectedViewNames({
4154
+ previousViews,
4155
+ currentViews,
4156
+ previousDependencies,
4157
+ currentDependencies,
4158
+ baseChanged: hints.statements.length > 0 || baseDdl.length > 0
4159
+ });
4160
+ const dropOrder = topologicalViewNames(
4161
+ new Set([...affected].filter((name) => previousViews.has(name))),
4162
+ previousDependencies
4163
+ ).reverse();
4164
+ const createOrder = topologicalViewNames(
4165
+ new Set([...affected].filter((name) => currentViews.has(name))),
4166
+ currentDependencies
4167
+ );
4168
+ const dropDdl = await generatedViewStatements({
4169
+ snapshot: hints.adjusted,
4170
+ names: new Set(dropOrder),
4171
+ mode: "drop",
4172
+ schema
4173
+ });
4174
+ const createDdl = await generatedViewStatements({
4175
+ snapshot,
4176
+ names: new Set(createOrder),
4177
+ mode: "create",
4178
+ schema
4179
+ });
4180
+ const currentViewDefinitions = new Map((canonical.views ?? []).map((view) => [view.view, view]));
4181
+ const statements = [
4182
+ ...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
4183
+ ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
4184
+ ...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
4185
+ ...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
4186
+ ...createOrder.map((name) => {
4187
+ const view = currentViewDefinitions.get(name);
4188
+ if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
4189
+ return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
4190
+ })
4191
+ ];
4192
+ return { namespace: canonical.namespace, schema, statements, snapshot };
4193
+ }
4194
+ function validateViewCatalogStatement({
4195
+ canonical,
4196
+ view,
4197
+ schema
4198
+ }) {
4199
+ const qualified = `${schema}.${view.view}`;
4200
+ const expectedColumns = sqlTextArray(view.columns.map((column) => column.columnName));
4201
+ const expectedTypes = `ARRAY[${view.columns.map((column) => `${viewTypeRegtype(column, schema)}::oid`).join(", ")}]::oid[]`;
4202
+ const relationIdentity = /* @__PURE__ */ new Map();
4203
+ for (const table of canonical.tables) {
4204
+ relationIdentity.set(table.table, `${schema}.${table.table}`);
4205
+ }
4206
+ for (const candidate of canonical.views ?? []) {
4207
+ relationIdentity.set(candidate.view, `${schema}.${candidate.view}`);
4208
+ }
4209
+ const expectedDependencies = sqlTextArray(
4210
+ view.dependencies.map((dependency) => {
4211
+ const physical = relationIdentity.get(dependency);
4212
+ if (!physical) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
4213
+ return physical;
4214
+ }).sort()
4215
+ );
4216
+ const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
4217
+ return `DO $sequence_orm_validate_view$
4218
+ DECLARE
4219
+ actual_columns text[];
4220
+ actual_types oid[];
4221
+ actual_dependencies text[];
4222
+ BEGIN
4223
+ SELECT
4224
+ array_agg(attribute.attname ORDER BY attribute.attnum),
4225
+ array_agg(attribute.atttypid ORDER BY attribute.attnum)
4226
+ INTO actual_columns, actual_types
4227
+ FROM pg_catalog.pg_attribute AS attribute
4228
+ WHERE attribute.attrelid = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4229
+ AND attribute.attnum > 0
4230
+ AND NOT attribute.attisdropped;
4231
+
4232
+ IF actual_columns IS DISTINCT FROM ${expectedColumns} THEN
4233
+ RAISE EXCEPTION 'managed view ${qualified} columns do not match its declaration: expected %, got %', ${expectedColumns}, actual_columns
4234
+ USING ERRCODE = '42804';
4235
+ END IF;
4236
+ IF actual_types IS DISTINCT FROM ${expectedTypes} THEN
4237
+ RAISE EXCEPTION 'managed view ${qualified} types do not match its declaration: expected %, got %', ${expectedTypes}, actual_types
4238
+ USING ERRCODE = '42804';
4239
+ END IF;
4240
+
4241
+ SELECT coalesce(array_agg(found.name ORDER BY found.name), ARRAY[]::text[])
4242
+ INTO actual_dependencies
4243
+ FROM (
4244
+ SELECT DISTINCT dependency_namespace.nspname || '.' || dependency_relation.relname AS name
4245
+ FROM pg_catalog.pg_rewrite AS rewrite
4246
+ JOIN pg_catalog.pg_depend AS dependency
4247
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
4248
+ AND dependency.objid = rewrite.oid
4249
+ AND dependency.refclassid = 'pg_catalog.pg_class'::regclass
4250
+ JOIN pg_catalog.pg_class AS dependency_relation ON dependency_relation.oid = dependency.refobjid
4251
+ JOIN pg_catalog.pg_namespace AS dependency_namespace ON dependency_namespace.oid = dependency_relation.relnamespace
4252
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4253
+ AND dependency.refobjid <> rewrite.ev_class
4254
+ AND dependency_relation.relkind IN ('r', 'p', 'v', 'm', 'f')
4255
+ ) AS found;
4256
+
4257
+ IF actual_dependencies IS DISTINCT FROM ${expectedDependencies} THEN
4258
+ RAISE EXCEPTION 'managed view ${qualified} dependencies do not match its declaration: expected %, got %', ${expectedDependencies}, actual_dependencies
4259
+ USING ERRCODE = '2BP01';
4260
+ END IF;
4261
+
4262
+ IF EXISTS (
4263
+ SELECT 1
4264
+ FROM pg_catalog.pg_rewrite AS rewrite
4265
+ JOIN pg_catalog.pg_depend AS dependency
4266
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
4267
+ AND dependency.objid = rewrite.oid
4268
+ AND dependency.refclassid = 'pg_catalog.pg_proc'::regclass
4269
+ JOIN pg_catalog.pg_proc AS dependency_function ON dependency_function.oid = dependency.refobjid
4270
+ JOIN pg_catalog.pg_namespace AS function_namespace ON function_namespace.oid = dependency_function.pronamespace
4271
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4272
+ AND (
4273
+ function_namespace.nspname <> 'pg_catalog'
4274
+ OR dependency_function.proname <> ALL (${allowedFunctions})
4275
+ )
4276
+ ) THEN
4277
+ RAISE EXCEPTION 'managed view ${qualified} may only call explicitly allowlisted side-effect-free pg_catalog functions'
4278
+ USING ERRCODE = '42501';
4279
+ END IF;
4280
+
4281
+ IF EXISTS (
4282
+ SELECT 1
4283
+ FROM pg_catalog.pg_rewrite AS rewrite
4284
+ JOIN pg_catalog.pg_depend AS dependency
4285
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
4286
+ AND dependency.objid = rewrite.oid
4287
+ AND dependency.refclassid = 'pg_catalog.pg_operator'::regclass
4288
+ JOIN pg_catalog.pg_operator AS dependency_operator ON dependency_operator.oid = dependency.refobjid
4289
+ JOIN pg_catalog.pg_namespace AS operator_namespace ON operator_namespace.oid = dependency_operator.oprnamespace
4290
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4291
+ AND operator_namespace.nspname <> 'pg_catalog'
4292
+ ) THEN
4293
+ RAISE EXCEPTION 'managed view ${qualified} may only use pg_catalog operators'
4294
+ USING ERRCODE = '42501';
4295
+ END IF;
4296
+ END
4297
+ $sequence_orm_validate_view$`;
4298
+ }
4299
+ function sqlTextArray(values) {
4300
+ return values.length === 0 ? "ARRAY[]::text[]" : `ARRAY[${values.map((value) => quoteLiteral(value)).join(", ")}]::text[]`;
4301
+ }
4302
+ function viewTypeRegtype(column, schema) {
4303
+ if (typeof column.type === "string") return `${quoteLiteral(sqlTypeName(column.type))}::regtype`;
4304
+ return `${quoteLiteral(`${quoteIdentifier(schema)}.${quoteIdentifier(column.type.enum)}`)}::regtype`;
4305
+ }
4306
+ var previousId = (snapshot) => snapshot.id;
4307
+ function snapshotViews(snapshot) {
4308
+ const views = snapshot.views ?? {};
4309
+ return new Map(Object.values(views).map((view) => [view.name, view]));
4310
+ }
4311
+ function snapshotViewDependencies(snapshot) {
4312
+ const metadata = snapshot._sequenceOrm;
4313
+ return metadata?.viewDependencies ?? {};
4314
+ }
4315
+ function withoutViews(snapshot) {
4316
+ const clone = structuredClone(snapshot);
4317
+ clone.views = {};
4318
+ delete clone._sequenceOrm;
4319
+ return clone;
4320
+ }
4321
+ function affectedViewNames({
4322
+ previousViews,
4323
+ currentViews,
4324
+ previousDependencies,
4325
+ currentDependencies,
4326
+ baseChanged
4327
+ }) {
4328
+ const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
4329
+ if (baseChanged) return allNames;
4330
+ const affected = new Set(
4331
+ [...allNames].filter((name) => {
4332
+ const before = previousViews.get(name);
4333
+ const after = currentViews.get(name);
4334
+ return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
4335
+ })
4336
+ );
4337
+ let grew = true;
4338
+ while (grew) {
4339
+ grew = false;
4340
+ for (const name of allNames) {
4341
+ if (affected.has(name)) continue;
4342
+ const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
4343
+ if ([...dependencies].some((dependency) => affected.has(dependency))) {
4344
+ affected.add(name);
4345
+ grew = true;
4346
+ }
4347
+ }
4348
+ }
4349
+ return affected;
4350
+ }
4351
+ function topologicalViewNames(names, dependencies) {
4352
+ const remaining = new Set(names);
4353
+ const ordered = [];
4354
+ while (remaining.size > 0) {
4355
+ const ready = [...remaining].filter((name) => (dependencies[name] ?? []).every((dependency) => !remaining.has(dependency))).sort();
4356
+ if (ready.length === 0) {
4357
+ throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining].sort().join(", ")}`);
4358
+ }
4359
+ for (const name of ready) {
4360
+ ordered.push(name);
4361
+ remaining.delete(name);
1291
4362
  }
1292
4363
  }
4364
+ return ordered;
4365
+ }
4366
+ async function generatedViewStatements({
4367
+ snapshot,
4368
+ names,
4369
+ mode,
4370
+ schema
4371
+ }) {
4372
+ if (names.size === 0) return /* @__PURE__ */ new Map();
4373
+ const without = structuredClone(snapshot);
4374
+ const holder = without;
4375
+ const views = holder.views ?? {};
4376
+ holder.views = Object.fromEntries(Object.entries(views).filter(([, view]) => !names.has(view.name)));
4377
+ const generated = mode === "drop" ? await diffStatements(snapshot, without) : await diffStatements(without, snapshot);
4378
+ const statements = /* @__PURE__ */ new Map();
4379
+ for (const name of names) {
4380
+ const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
4381
+ const prefix = mode === "drop" ? "DROP VIEW" : "CREATE VIEW";
4382
+ const statement = generated.find((candidate) => candidate.trimStart().startsWith(`${prefix} ${qualified}`));
4383
+ if (statement) statements.set(name, statement.replace(/;\s*$/, ""));
4384
+ }
4385
+ return statements;
1293
4386
  }
1294
- function tablesOf(snapshot) {
1295
- const tables = snapshot.tables ?? {};
1296
- return new Map(
1297
- Object.values(tables).map((table) => [`${table.schema}.${table.name}`, new Set(Object.keys(table.columns))])
1298
- );
4387
+ function requiredViewStatement(statements, name, schema, operation) {
4388
+ const statement = statements.get(name);
4389
+ if (!statement) {
4390
+ throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${name}'`);
4391
+ }
4392
+ return statement;
1299
4393
  }
1300
- function applyMigrationHints(previous, definition) {
1301
- const adjusted = structuredClone(previous);
1302
- const statements = [];
1303
- const schemaName = ormSchemaName(definition.namespace);
1304
- const qualified = (table) => `${quoteIdentifier(schemaName)}.${quoteIdentifier(table)}`;
1305
- for (const table of columnTables(definition)) {
1306
- const snapshotTable = findTable(adjusted, schemaName, table.table);
1307
- if (!snapshotTable) continue;
1308
- for (const [name, column] of Object.entries(table.columns ?? {})) {
1309
- if (column.renamed_from) {
1310
- const previousColumn = snapshotTable.columns[column.renamed_from];
1311
- if (previousColumn && snapshotTable.columns[name]) {
1312
- throw new OrmDefinitionError(
1313
- `'${table.table}.${name}' declares renamed_from '${column.renamed_from}', but both columns exist in the previous schema \u2014 a rename cannot overwrite a live column`
1314
- );
1315
- }
1316
- if (previousColumn) {
1317
- statements.push(
1318
- `ALTER TABLE ${qualified(table.table)} RENAME COLUMN ${quoteIdentifier(column.renamed_from)} TO ${quoteIdentifier(name)}`
1319
- );
1320
- delete snapshotTable.columns[column.renamed_from];
1321
- snapshotTable.columns[name] = { ...previousColumn, name };
1322
- } else if (!snapshotTable.columns[name]) {
1323
- throw new OrmDefinitionError(
1324
- `'${table.table}.${name}' declares renamed_from '${column.renamed_from}', but the previous schema has neither column \u2014 check the declared source name`
1325
- );
1326
- }
1327
- }
1328
- const cast = column.cast ? castParts(column.cast) : null;
1329
- if (cast) {
1330
- const current = snapshotTable.columns[name];
1331
- const targetType = sqlTypeName(column.type);
1332
- if (current && current.type !== targetType) {
1333
- statements.push(
1334
- `ALTER TABLE ${qualified(table.table)} ALTER COLUMN ${quoteIdentifier(name)} TYPE ${targetType} USING ${quoteIdentifier(name)}::${targetType}`
1335
- );
1336
- current.type = targetType;
1337
- }
4394
+ function assertValidViewDependencies(canonical) {
4395
+ const views = canonical.views ?? [];
4396
+ const known = /* @__PURE__ */ new Set([...canonical.tables.map((table) => table.table), ...views.map((view) => view.view)]);
4397
+ for (const view of views) {
4398
+ for (const dependency of view.dependencies) {
4399
+ if (!known.has(dependency)) {
4400
+ throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
1338
4401
  }
1339
4402
  }
1340
4403
  }
1341
- return { statements, adjusted };
1342
- }
1343
- function findTable(snapshot, schema, table) {
1344
- const tables = snapshot.tables ?? {};
1345
- return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
1346
- }
1347
- function snapshotTableNames(snapshot, schema) {
1348
- const tables = snapshot.tables ?? {};
1349
- return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
4404
+ topologicalViewNames(
4405
+ new Set(views.map((view) => view.view)),
4406
+ Object.fromEntries(views.map((view) => [view.view, view.dependencies]))
4407
+ );
1350
4408
  }
4409
+
4410
+ // src/engine/converge.ts
1351
4411
  var OWNED_STATEMENT = new RegExp(
1352
4412
  [
1353
4413
  String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
@@ -1360,18 +4420,43 @@ var OWNED_STATEMENT = new RegExp(
1360
4420
  ].join("|"),
1361
4421
  "i"
1362
4422
  );
4423
+ var V2_OWNED_STATEMENT = new RegExp(
4424
+ [
4425
+ String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
4426
+ String.raw`^\s*(GRANT|REVOKE)\b`,
4427
+ String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
4428
+ String.raw`\bROW LEVEL SECURITY\b`
4429
+ ].join("|"),
4430
+ "i"
4431
+ );
1363
4432
  function isOwnedStatement(statement) {
1364
4433
  return OWNED_STATEMENT.test(statement);
1365
4434
  }
4435
+ function isOwnedStatementV2(statement) {
4436
+ return V2_OWNED_STATEMENT.test(statement);
4437
+ }
1366
4438
  async function pendingKitStatements({
1367
4439
  definition,
1368
4440
  db
1369
4441
  }) {
1370
- const { imports, schemaName } = buildDrizzleEntities(definition);
4442
+ return pendingStatements({ entities: buildDrizzleEntities(definition), db, owned: OWNED_STATEMENT });
4443
+ }
4444
+ async function pendingKitStatementsV2({
4445
+ canonical,
4446
+ db
4447
+ }) {
4448
+ return pendingStatements({ entities: buildDrizzleEntitiesV2(canonical), db, owned: V2_OWNED_STATEMENT });
4449
+ }
4450
+ async function pendingStatements({
4451
+ entities,
4452
+ db,
4453
+ owned
4454
+ }) {
4455
+ const { imports, schemaName } = entities;
1371
4456
  const { statementsToExecute } = await withExitGuard(
1372
4457
  () => pushSchema(imports, shimExecute(db), [schemaName])
1373
4458
  );
1374
- return statementsToExecute.filter((statement) => !OWNED_STATEMENT.test(statement));
4459
+ return statementsToExecute.filter((statement) => !owned.test(statement));
1375
4460
  }
1376
4461
  function shimExecute(db) {
1377
4462
  const execute = db.execute.bind(db);
@@ -1405,7 +4490,7 @@ async function guardedRun(run) {
1405
4490
  }
1406
4491
 
1407
4492
  // src/engine/index.ts
1408
- var STATEMENT_KINDS = ["hint", "ddl", "security", "function"];
4493
+ var STATEMENT_KINDS = ["hint", "ddl", "security", "function", "synced_view"];
1409
4494
  async function planMigration({
1410
4495
  definition,
1411
4496
  previousSnapshot
@@ -1413,254 +4498,308 @@ async function planMigration({
1413
4498
  const schema = ormSchemaName(definition.namespace);
1414
4499
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(definition.namespace);
1415
4500
  const hints = applyMigrationHints(previous, definition);
1416
- const snapshot = generateSnapshot(definition, previousId(previous));
4501
+ const snapshot = generateSnapshot(definition, previousId2(previous));
1417
4502
  const ddl = await diffStatements(hints.adjusted, snapshot);
1418
4503
  const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
1419
4504
  const statements = [
1420
- ...policyDrops.map((sql2) => ({ kind: "security", sql: sql2 })),
1421
- ...hints.statements.map((sql2) => ({ kind: "hint", sql: sql2 })),
1422
- ...ddl.map((sql2) => ({ kind: "ddl", sql: sql2.replace(/;\s*$/, "") })),
1423
- ...securityStatements(definition).map((sql2) => ({ kind: "security", sql: sql2 })),
4505
+ ...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
4506
+ ...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
4507
+ ...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
4508
+ ...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
1424
4509
  // PREPARE each action's executes first (plpgsql bodies don't check columns
1425
4510
  // at CREATE), then create the functions. Both reference the tables the DDL
1426
4511
  // above creates, so they close the plan.
1427
- ...actionPrepareChecks(definition).map((sql2) => ({ kind: "function", sql: sql2 })),
1428
- ...actionFunctionStatements(definition).map((sql2) => ({ kind: "function", sql: sql2 }))
4512
+ ...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
4513
+ ...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
1429
4514
  ];
1430
4515
  return { namespace: definition.namespace, schema, statements, snapshot };
1431
4516
  }
1432
4517
  var compileNamespace = (definition) => planMigration({ definition });
1433
- var previousId = (snapshot) => snapshot.id;
1434
- function stableJson(value) {
1435
- if (value === null || typeof value !== "object") return JSON.stringify(value);
1436
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1437
- 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)}`);
1438
- return `{${entries.join(",")}}`;
1439
- }
1440
- function emptyToNull(value) {
1441
- if (value == null) return null;
1442
- if (Array.isArray(value)) return value.length === 0 ? null : value;
1443
- if (typeof value === "object" && Object.keys(value).length === 0) return null;
1444
- return value;
1445
- }
1446
- function canonicalTable(table) {
1447
- return {
1448
- table: table.table,
1449
- description: table.description ?? null,
1450
- columns: table.columns ?? null,
1451
- source: table.source ?? null,
1452
- links: emptyToNull(table.links),
1453
- indexes: emptyToNull(table.indexes),
1454
- policies: {
1455
- read: table.policies?.read?.source ?? null,
1456
- write: table.policies?.write?.source ?? null
1457
- },
1458
- actions: emptyToNull(
1459
- Object.fromEntries(
1460
- Object.entries(table.actions ?? {}).map(([name, action]) => [
1461
- name,
1462
- {
1463
- description: action.description ?? null,
1464
- requires: action.requires?.source ?? null,
1465
- params: emptyToNull(action.params),
1466
- validates: emptyToNull(action.validates?.map((entry) => entry.source)),
1467
- executes: action.executes,
1468
- returns: emptyToNull(action.returns),
1469
- audit: action.audit ?? null
1470
- }
1471
- ])
1472
- )
1473
- )
1474
- };
1475
- }
1476
- function canonicalCapabilities(capabilities) {
1477
- const { definitions, capabilities: declared } = capabilities.roles;
1478
- const isEmpty = Object.keys(definitions).length === 0 && Object.keys(declared).length === 0 && capabilities.uses.length === 0;
1479
- if (isEmpty) return null;
1480
- return {
1481
- roles: {
1482
- definitions: Object.fromEntries(
1483
- Object.entries(definitions).map(([key, def]) => [
1484
- key,
1485
- {
1486
- ...def.description !== void 0 ? { description: def.description } : {},
1487
- default: def.default,
1488
- inherits: [...def.inherits].sort(),
1489
- grants: [...def.grants].sort()
1490
- }
1491
- ])
1492
- ),
1493
- capabilities: Object.fromEntries(
1494
- Object.entries(declared).map(([key, cap]) => [
1495
- key,
1496
- cap.description !== void 0 ? { description: cap.description } : {}
1497
- ])
1498
- )
1499
- },
1500
- uses: [...capabilities.uses].sort()
1501
- };
1502
- }
1503
- function canonicalizeDefinition(definition) {
1504
- return {
1505
- namespace: definition.namespace,
1506
- description: definition.description,
1507
- capabilities: canonicalCapabilities(definition.capabilities),
1508
- tables: definition.tables.map(canonicalTable)
1509
- };
1510
- }
1511
- function computeDefinitionHash(definition) {
1512
- return createHash("sha256").update(HASH_DOMAIN).update(stableJson(canonicalizeDefinition(definition))).digest("hex");
1513
- }
1514
- var CUSTOM_ROLE_KEY_RE = /^[a-z0-9-]+$/;
1515
- var customRoleKeySchema = z.string().min(1).max(64).regex(
1516
- CUSTOM_ROLE_KEY_RE,
1517
- "Role and capability keys must match [a-z0-9-]+"
4518
+ var previousId2 = (snapshot) => snapshot.id;
4519
+ var IR_VERSION = 1;
4520
+ var CLAIMS_CONTRACT = {
4521
+ sub: "string",
4522
+ org: "string?",
4523
+ roles: "string[]",
4524
+ capabilities: "string[]"
4525
+ };
4526
+ var scalarLiteral = z.union([z.string(), z.number(), z.boolean(), z.null()]);
4527
+ var valueIrSchema = z.discriminatedUnion("kind", [
4528
+ z.object({ kind: z.literal("literal"), value: z.union([scalarLiteral, z.array(scalarLiteral)]) }),
4529
+ z.object({ kind: z.literal("variable"), name: z.string() }),
4530
+ z.object({ kind: z.literal("claim"), key: z.enum(["sub", "org"]) })
4531
+ ]);
4532
+ var CMP_OPS = [
4533
+ "eq",
4534
+ "neq",
4535
+ "gt",
4536
+ "gte",
4537
+ "lt",
4538
+ "lte",
4539
+ "in",
4540
+ "nin",
4541
+ "like",
4542
+ "ilike",
4543
+ "isNull",
4544
+ "contains",
4545
+ "hasKey"
4546
+ ];
4547
+ var predIrSchema = z.lazy(
4548
+ () => z.discriminatedUnion("kind", [
4549
+ z.object({ kind: z.literal("and"), operands: z.array(predIrSchema).min(1) }),
4550
+ z.object({ kind: z.literal("or"), operands: z.array(predIrSchema).min(1) }),
4551
+ z.object({ kind: z.literal("not"), operand: predIrSchema }),
4552
+ z.object({ kind: z.literal("cmp"), op: z.enum(CMP_OPS), column: z.string(), value: valueIrSchema }),
4553
+ z.object({ kind: z.literal("exists"), link: z.string(), where: predIrSchema }),
4554
+ z.object({
4555
+ kind: z.literal("aggPredicate"),
4556
+ link: z.string(),
4557
+ fn: z.literal("count"),
4558
+ op: z.enum(["eq", "neq", "gt", "gte", "lt", "lte"]),
4559
+ value: z.number(),
4560
+ where: predIrSchema.optional()
4561
+ }),
4562
+ z.object({ kind: z.literal("capability"), key: z.string() }),
4563
+ z.object({ kind: z.literal("role"), key: z.string() }),
4564
+ z.object({ kind: z.literal("dynamicFilter"), slot: z.string() }),
4565
+ z.object({ kind: z.literal("const"), value: z.boolean() })
4566
+ ])
1518
4567
  );
1519
- var MAX_CUSTOM_ROLE_DEFINITIONS = 50;
1520
- var MAX_CUSTOM_ROLE_CAPABILITIES = 100;
1521
- var customRoleCapabilityDeclarationSchema = z.object({
1522
- description: z.string().max(500).optional()
4568
+ var orderIrSchema = z.discriminatedUnion("kind", [
4569
+ z.object({
4570
+ kind: z.literal("column"),
4571
+ column: z.string(),
4572
+ /** Order through a to-one link: `link` is set and `column` is on the target. */
4573
+ link: z.string().optional(),
4574
+ direction: z.enum(["asc", "desc"]),
4575
+ nulls: z.enum(["first", "last"]).optional()
4576
+ }),
4577
+ /** §4f: `$orderBy: [FooOrderBy!]` variable — validated + lowered at runtime. */
4578
+ z.object({ kind: z.literal("dynamic"), slot: z.string() })
4579
+ ]);
4580
+ var limitIrSchema = z.object({
4581
+ /** Fixed value or a variable, clamped to [1, max]; absent → default. */
4582
+ variable: z.string().optional(),
4583
+ value: z.number().int().positive().optional(),
4584
+ default: z.number().int().positive(),
4585
+ max: z.number().int().positive()
1523
4586
  });
1524
- var customRoleDefinitionSchema = z.object({
1525
- description: z.string().max(500).optional(),
4587
+ var selectionIrSchema = z.lazy(
4588
+ () => z.object({
4589
+ typename: z.string(),
4590
+ columns: z.array(z.object({ alias: z.string(), column: z.string() })),
4591
+ relations: z.array(z.object({
4592
+ alias: z.string(),
4593
+ link: z.string(),
4594
+ selection: selectionIrSchema,
4595
+ where: predIrSchema.optional(),
4596
+ orderBy: z.array(orderIrSchema).optional(),
4597
+ limit: limitIrSchema.optional(),
4598
+ offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional()
4599
+ })),
4600
+ aggregates: z.array(z.object({
4601
+ alias: z.string(),
4602
+ link: z.string().optional(),
4603
+ where: predIrSchema.optional(),
4604
+ count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
4605
+ sum: z.array(z.string()).optional(),
4606
+ avg: z.array(z.string()).optional(),
4607
+ nodes: selectionIrSchema.optional()
4608
+ }))
4609
+ })
4610
+ );
4611
+ var includeIfSchema = z.object({ variable: z.string(), negated: z.boolean() });
4612
+ var selectRootSchema = z.object({
4613
+ kind: z.literal("select"),
4614
+ alias: z.string(),
4615
+ table: z.string(),
4616
+ selection: selectionIrSchema,
4617
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
4618
+ where: predIrSchema,
4619
+ orderBy: z.array(orderIrSchema).default([]),
4620
+ distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
4621
+ limit: limitIrSchema,
4622
+ offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
4623
+ includeIf: includeIfSchema.optional()
4624
+ });
4625
+ var byPkRootSchema = z.object({
4626
+ kind: z.literal("byPk"),
4627
+ alias: z.string(),
4628
+ table: z.string(),
4629
+ selection: selectionIrSchema,
4630
+ pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
4631
+ /** Policy predicate — ANDed with the PK match. */
4632
+ where: predIrSchema,
4633
+ includeIf: includeIfSchema.optional()
4634
+ });
4635
+ var aggregateRootSchema = z.object({
4636
+ kind: z.literal("aggregate"),
4637
+ alias: z.string(),
4638
+ table: z.string(),
4639
+ where: predIrSchema,
4640
+ count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
4641
+ sum: z.array(z.string()).optional(),
4642
+ avg: z.array(z.string()).optional(),
4643
+ nodes: selectionIrSchema.optional(),
4644
+ includeIf: includeIfSchema.optional()
4645
+ });
4646
+ var writeValuesSchema = z.union([
4647
+ z.object({ slot: z.string() }),
4648
+ z.object({ entries: z.array(z.object({ column: z.string(), value: valueIrSchema })) })
4649
+ ]);
4650
+ var onConflictIrSchema = z.union([
4651
+ z.object({ slot: z.string() }),
4652
+ z.object({ constraint: z.string(), updateColumns: z.array(z.string()) })
4653
+ ]);
4654
+ var mutationRootSchema = z.object({
4655
+ kind: z.literal("mutation"),
1526
4656
  /**
1527
- * Grants this role to the host's entire runtime audience. Resolved at the
1528
- * app layer at claim time — every caller who passes the host floor
1529
- * (`end_user`) holds the default roles; no tuple is written for it.
4657
+ * CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
4658
+ * synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
4659
+ * applies `set` — both statements generated by the executor from this
4660
+ * metadata, never authored SQL. `cowRevert` deletes the override so the
4661
+ * coalesce view falls back to synced. Both are gated on a matching synced
4662
+ * row and carry the write policy in `where` like update/delete.
1530
4663
  */
1531
- default: z.boolean().default(false),
1532
- /** Role keys whose grants this role also receives (privilege waterfall). */
1533
- inherits: z.array(customRoleKeySchema).default([]),
1534
- /** Capability keys this role grants directly. */
1535
- grants: z.array(customRoleKeySchema).default([])
4664
+ op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
4665
+ alias: z.string(),
4666
+ table: z.string(),
4667
+ /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
4668
+ cowSynced: z.string().optional(),
4669
+ /** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
4670
+ cowCopyColumns: z.array(z.string()).optional(),
4671
+ /** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
4672
+ cowCopyCheck: predIrSchema.optional(),
4673
+ /** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
4674
+ objects: z.union([
4675
+ z.object({ slot: z.string() }),
4676
+ z.object({ rows: z.array(z.array(z.object({ column: z.string(), value: valueIrSchema }))) })
4677
+ ]).optional(),
4678
+ /** Server-pinned columns (e.g. author_sub ← claim('sub')) merged into every inserted row. */
4679
+ pinnedColumns: z.array(z.object({ column: z.string(), value: valueIrSchema })).default([]),
4680
+ onConflict: onConflictIrSchema.optional(),
4681
+ set: writeValuesSchema.optional(),
4682
+ append: writeValuesSchema.optional(),
4683
+ inc: writeValuesSchema.optional(),
4684
+ /** update/delete row filter: AND(writePolicy, documentWhere/pk, dynamicFilter?). */
4685
+ where: predIrSchema.optional(),
4686
+ pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).optional(),
4687
+ /** WITH CHECK analogue evaluated against NEW row values (insert/update). */
4688
+ insertCheck: predIrSchema.optional(),
4689
+ returning: selectionIrSchema.nullable(),
4690
+ affectedRows: z.boolean().default(false),
4691
+ includeIf: includeIfSchema.optional()
1536
4692
  });
1537
- var customRolesBlockSchema = z.object({
1538
- /** Roles grantable to principals, keyed by role key. */
1539
- definitions: z.record(customRoleKeySchema, customRoleDefinitionSchema).default({}),
1540
- /** Leaf capability keys the host's code checks, keyed by capability key. */
1541
- capabilities: z.record(customRoleKeySchema, customRoleCapabilityDeclarationSchema).default({})
1542
- }).superRefine((block, ctx) => {
1543
- const roleKeys = Object.keys(block.definitions);
1544
- const capabilityKeys = new Set(Object.keys(block.capabilities));
1545
- if (roleKeys.length > MAX_CUSTOM_ROLE_DEFINITIONS) {
1546
- ctx.addIssue({
1547
- code: "custom",
1548
- message: `At most ${MAX_CUSTOM_ROLE_DEFINITIONS} role definitions are allowed`,
1549
- path: ["definitions"]
1550
- });
1551
- }
1552
- if (capabilityKeys.size > MAX_CUSTOM_ROLE_CAPABILITIES) {
1553
- ctx.addIssue({
1554
- code: "custom",
1555
- message: `At most ${MAX_CUSTOM_ROLE_CAPABILITIES} capabilities are allowed`,
1556
- path: ["capabilities"]
1557
- });
1558
- }
1559
- for (const key of roleKeys) {
1560
- if (capabilityKeys.has(key)) {
1561
- ctx.addIssue({
1562
- code: "custom",
1563
- message: `Key "${key}" is declared as both a role and a capability`,
1564
- path: ["definitions", key]
1565
- });
1566
- }
1567
- }
1568
- const roleKeySet = new Set(roleKeys);
1569
- for (const [roleKey, definition] of Object.entries(block.definitions)) {
1570
- const seenInherits = /* @__PURE__ */ new Set();
1571
- for (const inherited of definition.inherits) {
1572
- if (seenInherits.has(inherited)) {
1573
- ctx.addIssue({
1574
- code: "custom",
1575
- message: `Role "${roleKey}" lists "${inherited}" in inherits more than once`,
1576
- path: ["definitions", roleKey, "inherits"]
1577
- });
1578
- }
1579
- seenInherits.add(inherited);
1580
- if (!roleKeySet.has(inherited)) {
1581
- ctx.addIssue({
1582
- code: "custom",
1583
- message: `Role "${roleKey}" inherits unknown role "${inherited}"`,
1584
- path: ["definitions", roleKey, "inherits"]
1585
- });
1586
- }
1587
- }
1588
- const seenGrants = /* @__PURE__ */ new Set();
1589
- for (const granted of definition.grants) {
1590
- if (seenGrants.has(granted)) {
1591
- ctx.addIssue({
1592
- code: "custom",
1593
- message: `Role "${roleKey}" lists "${granted}" in grants more than once`,
1594
- path: ["definitions", roleKey, "grants"]
1595
- });
1596
- }
1597
- seenGrants.add(granted);
1598
- if (!capabilityKeys.has(granted)) {
1599
- ctx.addIssue({
1600
- code: "custom",
1601
- message: `Role "${roleKey}" grants unknown capability "${granted}"`,
1602
- path: ["definitions", roleKey, "grants"]
1603
- });
1604
- }
1605
- }
1606
- }
1607
- const state = /* @__PURE__ */ new Map();
1608
- const visit = (key, path) => {
1609
- if (state.get(key) === "done") return null;
1610
- if (state.get(key) === "visiting") return [...path, key];
1611
- state.set(key, "visiting");
1612
- const definition = block.definitions[key];
1613
- for (const inherited of definition?.inherits ?? []) {
1614
- if (!roleKeySet.has(inherited)) continue;
1615
- const cycle = visit(inherited, [...path, key]);
1616
- if (cycle) return cycle;
1617
- }
1618
- state.set(key, "done");
1619
- return null;
1620
- };
1621
- for (const roleKey of roleKeys) {
1622
- const cycle = visit(roleKey, []);
1623
- if (cycle) {
1624
- ctx.addIssue({
1625
- code: "custom",
1626
- message: `Role inheritance cycle: ${cycle.join(" -> ")}`,
1627
- path: ["definitions"]
1628
- });
1629
- break;
1630
- }
1631
- }
4693
+ var rootIrSchema = z.discriminatedUnion("kind", [
4694
+ selectRootSchema,
4695
+ byPkRootSchema,
4696
+ aggregateRootSchema,
4697
+ mutationRootSchema
4698
+ ]);
4699
+ var INPUT_REF_KINDS = [
4700
+ "boolExp",
4701
+ "orderBy",
4702
+ "selectColumn",
4703
+ "insertInput",
4704
+ "setInput",
4705
+ "appendInput",
4706
+ "onConflict"
4707
+ ];
4708
+ var variableDefaultIrSchema = z.object({
4709
+ value: z.union([scalarLiteral, z.array(scalarLiteral)])
1632
4710
  });
1633
- function customRolesDeclared(block) {
1634
- if (!block) return false;
1635
- return Object.keys(block.definitions).length > 0 || Object.keys(block.capabilities).length > 0;
1636
- }
1637
- var CUSTOM_CAPABILITY_PIN_RE = /^(?:org\/)?[a-z0-9-]+$/;
1638
- var MAX_CAPABILITY_PINS = 100;
1639
- var capabilityPinsSchema = z.array(
1640
- z.string().min(1).max(68).regex(
1641
- CUSTOM_CAPABILITY_PIN_RE,
1642
- 'Pins must be "org/<key>" or a bare "<key>" matching [a-z0-9-]+'
1643
- )
1644
- ).max(MAX_CAPABILITY_PINS).default([]);
1645
- function parseCapabilityPin(pin) {
1646
- return pin.startsWith("org/") ? { scope: "org", key: pin.slice("org/".length) } : { scope: "host", key: pin };
1647
- }
1648
- function validateCapabilityPins({
1649
- uses,
1650
- roles
1651
- }) {
1652
- const errors = [];
1653
- const seen = /* @__PURE__ */ new Set();
1654
- const roleKeys = new Set(Object.keys(roles?.definitions ?? {}));
1655
- for (const pin of uses) {
1656
- if (seen.has(pin)) errors.push(`Pin "${pin}" is listed more than once`);
1657
- seen.add(pin);
1658
- const parsed = parseCapabilityPin(pin);
1659
- if (parsed.scope === "host" && roleKeys.has(parsed.key)) {
1660
- errors.push(`Pin "${pin}" references a role \u2014 roles are never pinned`);
1661
- }
4711
+ var variableIrSchema = z.object({
4712
+ name: z.string(),
4713
+ type: z.union([
4714
+ z.object({ kind: z.literal("scalar"), scalar: z.enum(COLUMN_TYPES) }),
4715
+ /** `values` is the declared membership whitelist — the executor validates against it. */
4716
+ z.object({ kind: z.literal("enum"), name: z.string(), values: z.array(z.string()).min(1) }),
4717
+ /** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
4718
+ z.object({ kind: z.literal("inputRef"), input: z.enum(INPUT_REF_KINDS), table: z.string() })
4719
+ ]),
4720
+ nullable: z.boolean(),
4721
+ list: z.boolean(),
4722
+ /** Absent = no default declared; see {@link variableDefaultIrSchema}. */
4723
+ defaultValue: variableDefaultIrSchema.optional()
4724
+ });
4725
+ var compiledOperationSchema = z.object({
4726
+ irVersion: z.literal(IR_VERSION),
4727
+ name: z.string(),
4728
+ opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
4729
+ kind: z.enum(["query", "mutation"]),
4730
+ requires: z.object({
4731
+ capability: z.string().optional(),
4732
+ role: z.string().optional()
4733
+ }).nullable(),
4734
+ variables: z.array(variableIrSchema),
4735
+ /** Every table the op touches — data-token scoping + audit. */
4736
+ tableSet: z.array(z.string()).min(1),
4737
+ roots: z.array(rootIrSchema).min(1)
4738
+ });
4739
+ var tableMetaIrSchema = z.object({
4740
+ table: z.string(),
4741
+ schema: z.string(),
4742
+ typename: z.string(),
4743
+ /**
4744
+ * True when statements over this relation evaluate a claims-gated synced
4745
+ * view: synced twins (`source: gold.*`) materialize as views whose baked
4746
+ * policy reads `current_setting('request.jwt.claims')`, and namespace views
4747
+ * inherit the gate transitively through their declared dependencies. The
4748
+ * executor must stamp that GUC (transaction-scoped) before any statement
4749
+ * touching such a relation, or the synced arm yields zero rows regardless
4750
+ * of the compiled predicates.
4751
+ */
4752
+ claimsGated: z.boolean().default(false),
4753
+ columns: z.array(z.object({
4754
+ name: z.string(),
4755
+ columnName: z.string(),
4756
+ type: z.union([z.enum(COLUMN_TYPES), z.object({ enum: z.string(), values: z.array(z.string()) })]),
4757
+ nullable: z.boolean()
4758
+ })),
4759
+ /** Empty for keyless read-only views; keyed tables and views list logical columns. */
4760
+ primaryKey: z.array(z.string()),
4761
+ links: z.array(z.object({
4762
+ name: z.string(),
4763
+ kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
4764
+ target: z.string(),
4765
+ on: z.string(),
4766
+ references: z.string()
4767
+ })),
4768
+ /** §4f whitelists: what dynamic inputs may reference. */
4769
+ filterable: z.array(z.string()),
4770
+ orderable: z.array(z.string()),
4771
+ insertable: z.array(z.string()),
4772
+ settable: z.array(z.string()),
4773
+ appendable: z.array(z.string()),
4774
+ constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
4775
+ /** Depth/node caps for dynamic filter trees on this table. */
4776
+ dynamicFilterMaxDepth: z.number().int().positive().default(6),
4777
+ dynamicFilterMaxNodes: z.number().int().positive().default(64)
4778
+ });
4779
+ var compiledArtifactSchema = z.object({
4780
+ irVersion: z.literal(IR_VERSION),
4781
+ namespace: z.string(),
4782
+ schemaHash: z.string(),
4783
+ opsHash: z.string(),
4784
+ tables: z.record(z.string(), tableMetaIrSchema),
4785
+ operations: z.record(z.string(), compiledOperationSchema)
4786
+ });
4787
+ var IrVersionError = class extends Error {
4788
+ constructor(found) {
4789
+ super(`Unsupported IR version ${String(found)} \u2014 this executor supports up to ${IR_VERSION}. Upgrade @sequenceholdings/orm or re-push the namespace.`);
4790
+ this.name = "IrVersionError";
1662
4791
  }
1663
- return errors;
4792
+ };
4793
+ function normalizeIr(raw) {
4794
+ if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
4795
+ const version = raw.irVersion;
4796
+ if (version !== IR_VERSION) throw new IrVersionError(version);
4797
+ return compiledArtifactSchema.parse(raw);
4798
+ }
4799
+ function indexByHash(artifact) {
4800
+ const map = /* @__PURE__ */ new Map();
4801
+ for (const op of Object.values(artifact.operations)) map.set(op.opHash, op);
4802
+ return map;
1664
4803
  }
1665
4804
 
1666
4805
  // src/schema/namespace.ts
@@ -1785,6 +4924,8 @@ function validateCapabilityRefs(files, declared) {
1785
4924
  const referenced = [
1786
4925
  ...table.policies?.read ? referencedCapabilities(table.policies.read.ast) : [],
1787
4926
  ...table.policies?.write ? referencedCapabilities(table.policies.write.ast) : [],
4927
+ ...table.policies?.insert ? referencedCapabilities(table.policies.insert.ast) : [],
4928
+ ...table.policies?.update ? referencedCapabilities(table.policies.update.ast) : [],
1788
4929
  ...Object.values(table.actions ?? {}).flatMap((action) => [
1789
4930
  ...action.requires ? referencedCapabilities(action.requires.ast) : [],
1790
4931
  ...referencedExecuteCapabilities(action)
@@ -1918,8 +5059,8 @@ async function readMigrationWorkspace(dir, namespace) {
1918
5059
  journalPath
1919
5060
  );
1920
5061
  }
1921
- parsed.data.entries.forEach((entry, index2) => {
1922
- if (entry.ordinal !== index2 + 1) {
5062
+ parsed.data.entries.forEach((entry, index3) => {
5063
+ if (entry.ordinal !== index3 + 1) {
1923
5064
  throw new OrmDefinitionError(`migration journal entries must be contiguous from 1 (found ${entry.name})`, journalPath);
1924
5065
  }
1925
5066
  });
@@ -1942,7 +5083,7 @@ async function readMigrationSql(dir, entry) {
1942
5083
  throw new OrmDefinitionError("missing migration file", path);
1943
5084
  });
1944
5085
  }
1945
- var checksumOf = (text2) => createHash("sha256").update(text2).digest("hex");
5086
+ var checksumOf = (text4) => createHash("sha256").update(text4).digest("hex");
1946
5087
  var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
1947
5088
  function renderMigrationSql({
1948
5089
  name,
@@ -1968,12 +5109,12 @@ async function writeMigration({
1968
5109
  const tip = workspace.journal.entries.at(-1);
1969
5110
  const ordinal = (tip?.ordinal ?? 0) + 1;
1970
5111
  const name = migrationName(ordinal, contentHash);
1971
- const sql2 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
5112
+ const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
1972
5113
  const metaDir = join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
1973
5114
  await mkdir(metaDir, { recursive: true });
1974
5115
  const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
1975
5116
  `;
1976
- await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql2);
5117
+ await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
1977
5118
  await writeFile(join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
1978
5119
  const journal = {
1979
5120
  ...workspace.journal,
@@ -1984,7 +5125,7 @@ async function writeMigration({
1984
5125
  name,
1985
5126
  content_hash: contentHash,
1986
5127
  previous_content_hash: tip?.content_hash ?? null,
1987
- checksum: checksumOf(sql2),
5128
+ checksum: checksumOf(sql4),
1988
5129
  snapshot_checksum: checksumOf(snapshotText),
1989
5130
  destructive
1990
5131
  }
@@ -2096,11 +5237,11 @@ async function checkWorkspace({
2096
5237
  log(`no committed migrations for '${loaded.definition.namespace}' \u2014 run \`seq-studio orm diff\` and commit the result`);
2097
5238
  return 1;
2098
5239
  }
2099
- let previousId2;
5240
+ let previousId3;
2100
5241
  let previousHash = null;
2101
5242
  for (const entry of workspace.journal.entries) {
2102
- const sql2 = await readMigrationSql(dir, entry);
2103
- if (checksumOf(sql2) !== entry.checksum) {
5243
+ const sql4 = await readMigrationSql(dir, entry);
5244
+ if (checksumOf(sql4) !== entry.checksum) {
2104
5245
  log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
2105
5246
  return 1;
2106
5247
  }
@@ -2110,7 +5251,7 @@ async function checkWorkspace({
2110
5251
  return 1;
2111
5252
  }
2112
5253
  const snapshot = JSON.parse(snapshotText);
2113
- if (entry.ordinal > 1 && snapshot.prevId !== previousId2) {
5254
+ if (entry.ordinal > 1 && snapshot.prevId !== previousId3) {
2114
5255
  log(`snapshot chain broken at ${entry.name} \u2014 regenerate the workspace with \`seq-studio orm diff\``);
2115
5256
  return 1;
2116
5257
  }
@@ -2118,7 +5259,7 @@ async function checkWorkspace({
2118
5259
  log(`content-hash chain broken at ${entry.name} \u2014 migrations must be appended in order by \`seq-studio orm diff\``);
2119
5260
  return 1;
2120
5261
  }
2121
- previousId2 = snapshot.id;
5262
+ previousId3 = snapshot.id;
2122
5263
  previousHash = entry.content_hash;
2123
5264
  }
2124
5265
  if (tip.content_hash !== loaded.contentHash) {
@@ -2140,12 +5281,145 @@ async function checkWorkspace({
2140
5281
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
2141
5282
  return 0;
2142
5283
  }
2143
- var firstLine = (sql2) => sql2.split("\n")[0] ?? sql2;
5284
+ var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
5285
+
5286
+ // src/migrations/from-yaml.ts
5287
+ function convertV1DefinitionToCanonical(definition) {
5288
+ const raw = {
5289
+ definition_format: CANONICAL_FORMAT,
5290
+ namespace: definition.namespace,
5291
+ description: definition.description ?? void 0,
5292
+ enums: [],
5293
+ tables: definition.tables.map(convertTable),
5294
+ capabilities: {
5295
+ roles: definition.capabilities.roles,
5296
+ uses: [...definition.capabilities.uses]
5297
+ }
5298
+ };
5299
+ const parsed = canonicalDefinitionSchema.safeParse(raw);
5300
+ if (!parsed.success) {
5301
+ const issue = parsed.error.issues[0];
5302
+ throw new OrmDefinitionError(
5303
+ `converted definition is not valid canonical v2: ${issue ? `${issue.path.join(".")}: ${issue.message}` : "unknown issue"}`
5304
+ );
5305
+ }
5306
+ return normalizeCanonical(parsed.data);
5307
+ }
5308
+ function convertTable(table) {
5309
+ const columns = Object.entries(table.columns ?? {});
5310
+ const primaryKey2 = columns.filter(([, column]) => column.primary_key).map(([name]) => name);
5311
+ return {
5312
+ table: table.table,
5313
+ description: table.description,
5314
+ source: table.source,
5315
+ columns: columns.map(([name, column]) => convertColumn(name, column)),
5316
+ // Synced tables may declare a typed column/PK contract; untyped remain empty.
5317
+ primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
5318
+ links: Object.entries(table.links ?? {}).map(([name, link]) => convertLink(table.table, name, link)),
5319
+ indexes: (table.indexes ?? []).map((index3) => ({
5320
+ columns: [...index3.columns],
5321
+ unique: index3.unique ?? false,
5322
+ name: index3.name
5323
+ })),
5324
+ policies: {
5325
+ // Verbatim DSL source strings — the parsed v1 AST is derived state.
5326
+ read: table.policies?.read?.source,
5327
+ write: table.policies?.write?.source
5328
+ },
5329
+ // v2 has no action surface (writes are mutations); the transition sweep
5330
+ // drops the plpgsql action_* functions, so carrying the metadata would be
5331
+ // dead weight. droppedV1Actions() surfaces them for the emitter's TODO.
5332
+ actions: []
5333
+ };
5334
+ }
5335
+ function convertColumn(name, column) {
5336
+ const isDefaultFn = column.default !== void 0 && isFunctionDefault(column.default, column.type);
5337
+ return {
5338
+ name,
5339
+ columnName: name,
5340
+ type: column.type,
5341
+ nullable: column.primary_key ? false : column.nullable !== false,
5342
+ unique: column.unique === true,
5343
+ default: isDefaultFn ? void 0 : column.default,
5344
+ // isFunctionDefault guarantees one of the two allowlisted functions; the
5345
+ // canonicalDefinitionSchema parse below re-checks the enum.
5346
+ defaultFn: isDefaultFn ? column.default : void 0,
5347
+ description: column.description,
5348
+ renamedFrom: column.renamed_from,
5349
+ cast: column.cast,
5350
+ deprecated: column.deprecated === true
5351
+ };
5352
+ }
5353
+ function convertLink(table, name, link) {
5354
+ if (link.to.includes(".")) {
5355
+ throw new OrmDefinitionError(
5356
+ `link '${name}' on '${table}' targets '${link.to}' \u2014 cross-namespace links have no canonical v2 representation (links are namespace-local in v2)`
5357
+ );
5358
+ }
5359
+ return {
5360
+ name,
5361
+ kind: "toOne",
5362
+ target: link.to,
5363
+ on: link.on,
5364
+ enforce: link.enforce === true,
5365
+ description: link.description
5366
+ };
5367
+ }
5368
+
5369
+ // src/migrations/v2.ts
5370
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
5371
+ function rebaseViolations(statements, options) {
5372
+ const allowSecurity = options?.allowSecurityReemission ?? false;
5373
+ return statements.filter((statement) => !(statement.kind === "security" && allowSecurity));
5374
+ }
5375
+ var verifyRebaseIsNoop = (plan, options) => rebaseViolations(plan.statements, options).length === 0;
5376
+ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, snapshot }) {
5377
+ if (!SHA256_HEX.test(previousHash)) {
5378
+ throw new OrmDefinitionError(`rebase previousHash is not a sha256 hex digest: '${previousHash}'`);
5379
+ }
5380
+ if (!SHA256_HEX.test(newHash)) {
5381
+ throw new OrmDefinitionError(`rebase newHash is not a sha256 hex digest: '${newHash}'`);
5382
+ }
5383
+ if (previousHash === newHash) {
5384
+ throw new OrmDefinitionError("rebase previousHash and newHash are identical \u2014 nothing to rebase");
5385
+ }
5386
+ if (!Number.isInteger(ordinal) || ordinal < 2) {
5387
+ throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
5388
+ }
5389
+ const name = migrationName(ordinal, newHash);
5390
+ const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
5391
+ const snapshotText = `${JSON.stringify(snapshot, null, 2)}
5392
+ `;
5393
+ const entry = {
5394
+ ordinal,
5395
+ name,
5396
+ content_hash: newHash,
5397
+ previous_content_hash: previousHash,
5398
+ checksum: checksumOf(sql4),
5399
+ snapshot_checksum: checksumOf(snapshotText),
5400
+ destructive: false
5401
+ };
5402
+ return { name, entry, sql: sql4, snapshotText };
5403
+ }
5404
+ function renderRebaseMigrationSql({
5405
+ name,
5406
+ namespace,
5407
+ previousHash,
5408
+ newHash
5409
+ }) {
5410
+ return [
5411
+ `-- ${name}`,
5412
+ `-- namespace: ${namespace}`,
5413
+ `-- content hash: ${newHash}`,
5414
+ `-- rebase: v1 definitions (${previousHash}) converted to canonical v2 \u2014 verified no-op, zero statements`,
5415
+ "-- generated by `seq-studio orm diff --rebase` \u2014 review and commit, do not edit"
5416
+ ].join("\n") + "\n";
5417
+ }
2144
5418
 
2145
5419
  // src/schema/canonical.ts
2146
5420
  function parseCanonicalDefinition(value) {
2147
5421
  const canonical = asCanonicalShape(value);
2148
- const files = canonical.tables.map((table, index2) => {
5422
+ const files = canonical.tables.map((table, index3) => {
2149
5423
  const input = {
2150
5424
  namespace: canonical.namespace,
2151
5425
  table: table["table"],
@@ -2162,10 +5436,10 @@ function parseCanonicalDefinition(value) {
2162
5436
  const issue = parsed.error.issues[0];
2163
5437
  throw new OrmDefinitionError(
2164
5438
  `stored definition for table ${String(table["table"])} is invalid: ${issue?.message ?? "unknown issue"}`,
2165
- `canonical[${index2}]`
5439
+ `canonical[${index3}]`
2166
5440
  );
2167
5441
  }
2168
- return { file: `canonical[${index2}]`, table: parsed.data };
5442
+ return { file: `canonical[${index3}]`, table: parsed.data };
2169
5443
  });
2170
5444
  const config = namespaceConfigSchema.safeParse({
2171
5445
  namespace: canonical.namespace,
@@ -2198,8 +5472,9 @@ function canonicalPolicies(value) {
2198
5472
  if (value === null || typeof value !== "object") return void 0;
2199
5473
  const record = value;
2200
5474
  const policies = {};
2201
- if (typeof record["read"] === "string") policies["read"] = record["read"];
2202
- if (typeof record["write"] === "string") policies["write"] = record["write"];
5475
+ for (const kind of ["read", "write", "insert", "update"]) {
5476
+ if (typeof record[kind] === "string") policies[kind] = record[kind];
5477
+ }
2203
5478
  return Object.keys(policies).length > 0 ? policies : void 0;
2204
5479
  }
2205
5480
  function canonicalActions(value) {
@@ -2222,6 +5497,6 @@ function canonicalActions(value) {
2222
5497
  );
2223
5498
  }
2224
5499
 
2225
- export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CLAIMS_SETTING, CLAIM_KEY_PATTERN, COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, DEFINITION_FILE_EXTENSIONS, HASH_DOMAIN, IDENTIFIER_PATTERN, MAX_DEFINITION_FILE_BYTES, MAX_IDENTIFIER_LENGTH, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, STATEMENT_KINDS, SYNCED_SOURCE_PATTERN, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, applyMigrationHints, buildActionParamsSchema, buildDrizzleEntities, buildNamespaceDefinition, canonicalizeDefinition, capabilityExpression, capabilityPinsSchema, castParts, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredicate, computeDefinitionHash, consumerRoleName, customRolesBlockSchema, customRolesDeclared, declaredCapabilityKeys, diffCommand, diffStatements, emptyJournal, emptySnapshot, extractNamedParams, generateNamespaceTypes, generateSnapshot, identifierSchema, isFunctionDefault, isOwnedStatement, isQualifiedReference, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, migrationName, namespaceConfigSchema, namespaceSchema, normalizeSnapshot, ormSchemaName, parseCanonicalDefinition, parseCapabilityPin, parseDefinitionSources, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, planMigration, predicateOperands, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, referencedCapabilities, referencedColumns, renderMigrationSql, securityStatements, sqlTypeName, stableJson, tableFileSchema, tableReferenceSchema, typesInterfaceName, validateCapabilityPins, walkPredicate, writeMigration };
5500
+ export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CANONICAL_FORMAT, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CLAIMS_CONTRACT, CLAIMS_SETTING, CLAIM_KEY_PATTERN, CMP_OPS, COLUMN_BRAND, COLUMN_DEFAULT_FNS, COLUMN_TYPES2 as COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, ColumnBuilder, DEFINITION_FILE_EXTENSIONS, ENUM_BRAND, HASH_DOMAIN, IDENTIFIER_PATTERN, IDENTIFIER_RE, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LINK_BRAND, MAX_DEFINITION_FILE_BYTES, MAX_IDENTIFIER_LENGTH, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, ORM_TO_PIPELINE_TYPE, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, ROLES_CLAIM_KEY, SCHEMA_DIR, STATEMENT_KINDS, SYNCED_CONTRACT_HASH_DOMAIN, SYNCED_SOURCE_PATTERN, SYNCED_VIEW_EXECUTION_OWNER_ROLE, TABLE_BRAND, VIEW_BRAND, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, activeAuthoringContext, applyMigrationHints, applyMigrationHintsV2, beginAuthoringContext, buildActionParamsSchema, buildDrizzleEntities, buildDrizzleEntitiesV2, buildNamespaceDefinition, buildRebaseJournalEntry, canonicalActionSchema, canonicalColumnSchema, canonicalDefinitionSchema, canonicalEnumSchema, canonicalIndexSchema, canonicalLinkSchema, canonicalPoliciesSchema, canonicalRelations, canonicalTableSchema, canonicalViewSchema, canonicalizeDefinition, capabilityExpression, castParts, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema2 as columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredIrToSql, compilePredicate, compiledArtifactSchema, compiledOperationSchema, computeCanonicalHash, computeDefinitionHash, computeSyncedContractHash, consumerRoleName, convertV1DefinitionToCanonical, declaredCapabilityKeys, defineEnum, defineNamespace, defineSyncedTable, defineTable, defineView, deriveColumnName, diffCommand, diffStatements, emptyJournal, emptySnapshot, endAuthoringContext, extractNamedParams, extractOrmSyncedTableContract, generateNamespaceTypes, generateSnapshot, generateSnapshotV2, identifierSchema, indexByHash, isColumnBuilder, isEnumRef, isFunctionDefault, isOwnedStatement, isOwnedStatementV2, isQualifiedReference, isSeqLink, isSeqNamespaceConfig, isSeqTable, isSeqView, limitIrSchema, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, loadNamespaceFromDir, migrationName, namespaceConfigSchema, namespaceSchema, normalizeCanonical, normalizeIr, normalizeSnapshot, orderIrSchema, ormSchemaName, overlayWinsUnion, overrideRowMutation, parseCanonicalDefinition, parseDefinitionSources, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, pendingKitStatementsV2, planMigration, planMigrationV2, policyExpressionSchema, policyVariantSchema, predIrSchema, predicateOperands, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, rebaseViolations, referencedCapabilities, referencedColumns, renderMigrationSql, revertOverlayMutation, rewriteSyncedSourceRelationsInViewQuery, roleExpression, rootIrSchema, securityStatements, selectionIrSchema, serializeNamespace, sqlTypeName, stableJson, syncedBackingRevokeStatements, syncedContractSurface, syncedTables, syncedViewDropStatements, syncedViewStatements, t, tableFileSchema, tableMetaIrSchema, tableReferenceSchema, toMany, toOne, toPlatformUser, typesInterfaceName, validateViewCatalogStatement, valueIrSchema, variableDefaultIrSchema, variableIrSchema, verifyRebaseIsNoop, walkPredicate, writeMigration };
2226
5501
  //# sourceMappingURL=index.js.map
2227
5502
  //# sourceMappingURL=index.js.map