@sequenceholdings/orm 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +57 -2
  2. package/dist/authoring.cjs +122 -15
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +20 -3
  5. package/dist/authoring.d.ts +20 -3
  6. package/dist/authoring.js +122 -15
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +83 -9
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +47 -1
  11. package/dist/canonical.d.ts +47 -1
  12. package/dist/canonical.js +83 -10
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +721 -132
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +723 -134
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs.map +1 -1
  19. package/dist/client.js.map +1 -1
  20. package/dist/compiler.cjs +482 -96
  21. package/dist/compiler.cjs.map +1 -1
  22. package/dist/compiler.d.cts +34 -2
  23. package/dist/compiler.d.ts +34 -2
  24. package/dist/compiler.js +483 -97
  25. package/dist/compiler.js.map +1 -1
  26. package/dist/index.cjs +414 -70
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +266 -9
  29. package/dist/index.d.ts +266 -9
  30. package/dist/index.js +404 -72
  31. package/dist/index.js.map +1 -1
  32. package/dist/ir.cjs +146 -12
  33. package/dist/ir.cjs.map +1 -1
  34. package/dist/ir.d.cts +161 -5
  35. package/dist/ir.d.ts +161 -5
  36. package/dist/ir.js +144 -13
  37. package/dist/ir.js.map +1 -1
  38. package/dist/{naming-CTvWSRTf.d.ts → naming-DCn94LJ4.d.ts} +15 -1
  39. package/dist/{naming-DRyinU4u.d.cts → naming-DPglwzkB.d.cts} +15 -1
  40. package/dist/predicate-v2.cjs +130 -11
  41. package/dist/predicate-v2.cjs.map +1 -1
  42. package/dist/predicate-v2.js +130 -11
  43. package/dist/predicate-v2.js.map +1 -1
  44. package/dist/sdl.cjs +460 -13
  45. package/dist/sdl.cjs.map +1 -1
  46. package/dist/sdl.d.cts +1 -1
  47. package/dist/sdl.d.ts +1 -1
  48. package/dist/sdl.js +459 -14
  49. package/dist/sdl.js.map +1 -1
  50. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -36,6 +36,7 @@ var ColumnBuilder = class {
36
36
  hasDefault: false,
37
37
  defaultValue: void 0,
38
38
  defaultFn: void 0,
39
+ onUpdateFn: void 0,
39
40
  description: void 0,
40
41
  renamedFrom: void 0,
41
42
  cast: void 0,
@@ -71,6 +72,15 @@ var ColumnBuilder = class {
71
72
  this.state.defaultFn = void 0;
72
73
  return this;
73
74
  }
75
+ /**
76
+ * Stamp this column with Postgres `now()` on every UPDATE. Only valid on
77
+ * timestamptz/date; the compiler drops it from SetInput so callers cannot
78
+ * supply a value.
79
+ */
80
+ onUpdate(value) {
81
+ this.state.onUpdateFn = value;
82
+ return this;
83
+ }
74
84
  description(text4) {
75
85
  this.state.description = text4;
76
86
  return this;
@@ -114,6 +124,7 @@ var ColumnBuilder = class {
114
124
  };
115
125
  if (this.state.hasDefault) column.default = this.state.defaultValue ?? null;
116
126
  if (this.state.defaultFn !== void 0) column.defaultFn = this.state.defaultFn;
127
+ if (this.state.onUpdateFn !== void 0) column.onUpdateFn = this.state.onUpdateFn;
117
128
  if (this.state.description !== void 0) column.description = this.state.description;
118
129
  if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
119
130
  if (this.state.cast !== void 0) column.cast = this.state.cast;
@@ -141,6 +152,7 @@ var t = {
141
152
  // src/constants.ts
142
153
  var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
143
154
  var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
155
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
144
156
  var MAX_IDENTIFIER_LENGTH = 63;
145
157
  var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
146
158
  var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
@@ -156,6 +168,8 @@ var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
156
168
  var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
157
169
  var HASH_DOMAIN = "sequence-orm-definitions\0";
158
170
  var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
171
+ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
172
+ var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
159
173
  var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
160
174
  var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
161
175
 
@@ -264,10 +278,10 @@ function isSeqLink(value) {
264
278
  function isSeqTable(value) {
265
279
  return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
266
280
  }
267
- function assertIdentifier({ value, what, file }) {
268
- if (!IDENTIFIER_PATTERN.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
281
+ function assertIdentifier({ value, what, file, pattern = IDENTIFIER_PATTERN }) {
282
+ if (!pattern.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
269
283
  throw new OrmDefinitionError(
270
- `${what} "${value}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
284
+ `${what} "${value}" must match ${pattern} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
271
285
  file
272
286
  );
273
287
  }
@@ -288,7 +302,7 @@ function defineTable(name, columns, config = {}) {
288
302
  );
289
303
  }
290
304
  const columnName = builder.columnNameFor(propertyName);
291
- assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file });
305
+ assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
292
306
  const existing = seenColumnNames.get(columnName);
293
307
  if (existing !== void 0) {
294
308
  throw new OrmDefinitionError(
@@ -328,7 +342,8 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
328
342
  assertIdentifier({
329
343
  value: columnName,
330
344
  what: `synced table "${name}": column "${propertyName}" column name`,
331
- file
345
+ file,
346
+ pattern: COLUMN_IDENTIFIER_PATTERN
332
347
  });
333
348
  const existing = seenColumnNames.get(columnName);
334
349
  if (existing !== void 0) {
@@ -417,7 +432,7 @@ function defineView(name, columns, config) {
417
432
  );
418
433
  }
419
434
  const columnName = builder.columnNameFor(propertyName);
420
- if (!IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
435
+ if (!COLUMN_IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
421
436
  throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
422
437
  }
423
438
  const existing = seenColumnNames.get(columnName);
@@ -866,6 +881,7 @@ function scalarColumnType(table, property, helper) {
866
881
  var CANONICAL_FORMAT = 2;
867
882
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
868
883
  var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
884
+ var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
869
885
  var COLUMN_TYPES = [
870
886
  "uuid",
871
887
  "text",
@@ -886,18 +902,33 @@ var canonicalEnumSchema = zod.z.object({
886
902
  var canonicalColumnSchema = zod.z.object({
887
903
  /** Property name in TS (camelCase); columnName is the Postgres name. */
888
904
  name: zod.z.string().min(1).max(63),
889
- columnName: identifier,
905
+ columnName: columnIdentifier,
890
906
  type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
891
907
  nullable: zod.z.boolean().default(true),
892
908
  unique: zod.z.boolean().default(false),
893
909
  /** Literal default, or one of the allowlisted SQL functions. */
894
910
  default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
895
911
  defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
912
+ /**
913
+ * Stamp this column from the database clock on UPDATE (`now()`). The
914
+ * column stays insertable (defaults fill gaps) but is omitted from
915
+ * SetInput / settable so callers cannot backdate it.
916
+ */
917
+ onUpdateFn: zod.z.enum(["now()"]).optional(),
896
918
  description: zod.z.string().max(500).optional(),
897
919
  /** Migration hints — same semantics as v1. */
898
- renamedFrom: identifier.optional(),
920
+ renamedFrom: columnIdentifier.optional(),
899
921
  cast: zod.z.string().max(200).optional(),
900
922
  deprecated: zod.z.boolean().default(false)
923
+ }).superRefine((column, ctx) => {
924
+ if (column.onUpdateFn === void 0) return;
925
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
926
+ ctx.addIssue({
927
+ code: "custom",
928
+ path: ["onUpdateFn"],
929
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
930
+ });
931
+ }
901
932
  });
902
933
  var canonicalLinkSchema = zod.z.object({
903
934
  name: zod.z.string().min(1).max(63),
@@ -905,15 +936,15 @@ var canonicalLinkSchema = zod.z.object({
905
936
  /** Target table name (namespace-local). Absent for toPlatformUser. */
906
937
  target: identifier.optional(),
907
938
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
908
- on: identifier,
939
+ on: columnIdentifier,
909
940
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
910
- references: identifier.optional(),
941
+ references: columnIdentifier.optional(),
911
942
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
912
943
  enforce: zod.z.boolean().default(false),
913
944
  description: zod.z.string().max(500).optional()
914
945
  });
915
946
  var canonicalIndexSchema = zod.z.object({
916
- columns: zod.z.array(identifier).min(1),
947
+ columns: zod.z.array(columnIdentifier).min(1),
917
948
  unique: zod.z.boolean().default(false),
918
949
  name: identifier.optional()
919
950
  });
@@ -1161,6 +1192,13 @@ var canonicalCowMutationSchema = zod.z.object({
1161
1192
  }
1162
1193
  }
1163
1194
  });
1195
+ var canonicalForeignKeySchema = zod.z.object({
1196
+ name: identifier,
1197
+ columns: zod.z.array(columnIdentifier).min(1).max(8),
1198
+ target: identifier,
1199
+ references: zod.z.array(columnIdentifier).min(1).max(8),
1200
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
1201
+ }).strict();
1164
1202
  var canonicalTableSchema = zod.z.object({
1165
1203
  table: identifier,
1166
1204
  description: zod.z.string().max(2e3).optional(),
@@ -1169,7 +1207,7 @@ var canonicalTableSchema = zod.z.object({
1169
1207
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
1170
1208
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
1171
1209
  columns: zod.z.array(canonicalColumnSchema).default([]),
1172
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
1210
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
1173
1211
  links: zod.z.array(canonicalLinkSchema).default([]),
1174
1212
  indexes: zod.z.array(canonicalIndexSchema).default([]),
1175
1213
  policies: canonicalPoliciesSchema.default({}),
@@ -1180,14 +1218,16 @@ var canonicalTableSchema = zod.z.object({
1180
1218
  */
1181
1219
  actions: zod.z.array(canonicalActionSchema).default([]),
1182
1220
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
1183
- cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
1221
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
1222
+ /** Omitted by old definitions, preserving their existing content hashes. */
1223
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
1184
1224
  });
1185
1225
  var canonicalViewSchema = zod.z.object({
1186
1226
  view: identifier,
1187
1227
  description: zod.z.string().max(2e3).optional(),
1188
1228
  columns: zod.z.array(canonicalColumnSchema).min(1),
1189
1229
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
1190
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
1230
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
1191
1231
  links: zod.z.array(canonicalLinkSchema).default([]),
1192
1232
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
1193
1233
  query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
@@ -1200,6 +1240,9 @@ var canonicalViewSchema = zod.z.object({
1200
1240
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1201
1241
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
1202
1242
  }
1243
+ if (column.onUpdateFn !== void 0) {
1244
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare onUpdateFn` });
1245
+ }
1203
1246
  if (column.unique) {
1204
1247
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
1205
1248
  }
@@ -1233,6 +1276,45 @@ var canonicalDefinitionSchema = zod.z.object({
1233
1276
  ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
1234
1277
  }
1235
1278
  seen.add(table.table);
1279
+ if (table.source !== void 0) {
1280
+ for (const [columnIndex, column] of table.columns.entries()) {
1281
+ if (column.onUpdateFn !== void 0) {
1282
+ ctx.addIssue({
1283
+ code: "custom",
1284
+ path: ["tables", index3, "columns", columnIndex],
1285
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
1286
+ });
1287
+ }
1288
+ }
1289
+ }
1290
+ }
1291
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
1292
+ for (const [tableIndex, table] of definition.tables.entries()) {
1293
+ const names = /* @__PURE__ */ new Set([
1294
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
1295
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
1296
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
1297
+ ]);
1298
+ for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
1299
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
1300
+ if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
1301
+ names.add(fk.name);
1302
+ const target = fkTables.get(fk.target);
1303
+ if (table.source || !target || target.source) {
1304
+ fail("foreign keys must connect owned tables in the same namespace");
1305
+ continue;
1306
+ }
1307
+ if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
1308
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
1309
+ for (const [position, name] of fk.columns.entries()) {
1310
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
1311
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
1312
+ if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
1313
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
1314
+ }
1315
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
1316
+ if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
1317
+ }
1236
1318
  }
1237
1319
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
1238
1320
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -1368,12 +1450,13 @@ function normalizeCanonical(definition) {
1368
1450
  ...definition,
1369
1451
  enums: sortByName(definition.enums),
1370
1452
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
1371
- const { cowMutations, ...rest } = t2;
1453
+ const { cowMutations, foreignKeys, ...rest } = t2;
1372
1454
  return {
1373
1455
  ...rest,
1374
1456
  links: sortByName(t2.links),
1375
1457
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
1376
1458
  actions: sortByName(t2.actions),
1459
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1377
1460
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
1378
1461
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
1379
1462
  };
@@ -1647,6 +1730,22 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1647
1730
  } else if (columnLevelPk.length > 0) {
1648
1731
  candidate.primaryKey = columnLevelPk;
1649
1732
  }
1733
+ if (table.config.foreignKeys?.length) {
1734
+ if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
1735
+ candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
1736
+ const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
1737
+ throw new OrmDefinitionError(message, table.file);
1738
+ } });
1739
+ if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
1740
+ return {
1741
+ name: fk.name,
1742
+ columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
1743
+ target: target.tableName,
1744
+ references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
1745
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {}
1746
+ };
1747
+ });
1748
+ }
1650
1749
  for (const index3 of table.config.indexes ?? []) {
1651
1750
  const entry = {
1652
1751
  columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1699,6 +1798,12 @@ function buildCanonicalView({
1699
1798
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1700
1799
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1701
1800
  }
1801
+ if (column.onUpdateFn !== void 0) {
1802
+ throw new OrmDefinitionError(
1803
+ `view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
1804
+ view.file
1805
+ );
1806
+ }
1702
1807
  if (column.unique) {
1703
1808
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1704
1809
  }
@@ -1959,6 +2064,10 @@ async function loadNamespaceFromDir(dir, options = {}) {
1959
2064
  fsCache: false,
1960
2065
  ...options.ormModulePath !== void 0 ? {
1961
2066
  alias: {
2067
+ // The public /drizzle barrel re-exports this pinned dependency.
2068
+ // Explicitly map it before the root alias: otherwise jiti appends
2069
+ // "/drizzle" to the overridden module's filename.
2070
+ "@sequenceholdings/orm/drizzle": module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).resolve("drizzle-orm"),
1962
2071
  "@sequenceholdings/orm": options.ormModulePath,
1963
2072
  "@sequenceholdings/orm/authoring": options.ormModulePath
1964
2073
  }
@@ -2521,8 +2630,8 @@ var actionSchema = zod.z.strictObject({
2521
2630
  }
2522
2631
  }
2523
2632
  });
2524
- function extractNamedParams(sql4) {
2525
- const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
2633
+ function extractNamedParams(sql5) {
2634
+ const withoutStrings = sql5.replace(QUOTED_STRING_PATTERN, "''");
2526
2635
  const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
2527
2636
  return [...new Set(names)];
2528
2637
  }
@@ -2679,8 +2788,8 @@ var actionParamArg = (name) => `p_${name}`;
2679
2788
  function compileActionStatements(action, renderParam = actionParamArg) {
2680
2789
  return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
2681
2790
  }
2682
- function compileStatement(sql4, renderParam) {
2683
- const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
2791
+ function compileStatement(sql5, renderParam) {
2792
+ const compiled = sql5.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
2684
2793
  if (claimKey !== void 0) {
2685
2794
  if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
2686
2795
  return claimExpression(claimKey);
@@ -2845,8 +2954,8 @@ function actionPrepareChecks(definition) {
2845
2954
  const order = actionParamOrder(action);
2846
2955
  const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
2847
2956
  const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
2848
- compiled.forEach((sql4, index3) => {
2849
- const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
2957
+ compiled.forEach((sql5, index3) => {
2958
+ const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql5) : sql5;
2850
2959
  statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
2851
2960
  });
2852
2961
  }
@@ -3300,7 +3409,8 @@ function syncedViewStatements({
3300
3409
  readPolicyIrByTable,
3301
3410
  columnNamesByTable,
3302
3411
  columnTypesByTable,
3303
- executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
3412
+ executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE,
3413
+ backingPrivileges = "grant"
3304
3414
  }) {
3305
3415
  const schema = quoteIdentifier(ormSchemaName(definition.namespace));
3306
3416
  const role = quoteIdentifier(consumerRoleName(definition.namespace));
@@ -3334,8 +3444,10 @@ function syncedViewStatements({
3334
3444
  columnTypes,
3335
3445
  columnNames: columnNamesByTable?.get(table.table)
3336
3446
  })}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
3337
- statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical.schema)} TO ${owner}`);
3338
- statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
3447
+ if (backingPrivileges === "grant") {
3448
+ statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical.schema)} TO ${owner}`);
3449
+ statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
3450
+ }
3339
3451
  statements.push(
3340
3452
  `CREATE OR REPLACE VIEW ${viewName} AS SELECT * FROM ${physicalTarget}${whereClause}`
3341
3453
  );
@@ -3624,7 +3736,7 @@ var FACTORIES2 = {
3624
3736
  var SCHEMA_IMPORT_KEY2 = "$schema";
3625
3737
  var enumImportKey = (name) => `$enum_${name}`;
3626
3738
  var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
3627
- function buildDrizzleEntitiesV2(definition) {
3739
+ function buildDrizzleEntitiesV2(definition, options = {}) {
3628
3740
  const schemaName = ormSchemaName(definition.namespace);
3629
3741
  const schema = pgCore.pgSchema(schemaName);
3630
3742
  const imports = { [SCHEMA_IMPORT_KEY2]: schema };
@@ -3639,11 +3751,19 @@ function buildDrizzleEntitiesV2(definition) {
3639
3751
  }
3640
3752
  const byName = new Map(definition.tables.map((table) => [table.table, table]));
3641
3753
  for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
3642
- const drizzleTable = buildTable2(schema, table, built, enums, byName);
3754
+ const drizzleTable = buildTable2(schema, table, built, enums, byName, options.introspection ?? false);
3643
3755
  built.set(table.table, drizzleTable);
3644
3756
  imports[table.table] = drizzleTable;
3645
3757
  tables[table.table] = drizzleTable;
3646
3758
  }
3759
+ if (options.introspection) {
3760
+ for (const table of definition.tables.filter((table2) => table2.source)) {
3761
+ const columns = Object.fromEntries(
3762
+ table.columns.map((column) => [column.columnName, buildViewColumnV2({ view: table.table, column, enums })])
3763
+ );
3764
+ imports[table.table] = schema.view(table.table, columns).existing();
3765
+ }
3766
+ }
3647
3767
  for (const view of definition.views ?? []) {
3648
3768
  const columns = Object.fromEntries(
3649
3769
  view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
@@ -3711,18 +3831,18 @@ function rewriteSyncedSourceRelationsInViewQuery({
3711
3831
  return rewritten;
3712
3832
  });
3713
3833
  }
3714
- function transformSqlOutsideLiterals(sql4, transform) {
3834
+ function transformSqlOutsideLiterals(sql5, transform) {
3715
3835
  const out = [];
3716
3836
  let state = { kind: "normal" };
3717
3837
  let i = 0;
3718
3838
  let codeStart = 0;
3719
3839
  const emitCode = (end) => {
3720
- if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
3840
+ if (end > codeStart) out.push(transform(sql5.slice(codeStart, end)));
3721
3841
  codeStart = end;
3722
3842
  };
3723
- while (i < sql4.length) {
3724
- const c = sql4[i];
3725
- const next = i + 1 < sql4.length ? sql4[i + 1] : "";
3843
+ while (i < sql5.length) {
3844
+ const c = sql5[i];
3845
+ const next = i + 1 < sql5.length ? sql5[i + 1] : "";
3726
3846
  switch (state.kind) {
3727
3847
  case "normal": {
3728
3848
  if (c === "-" && next === "-") {
@@ -3742,7 +3862,7 @@ function transformSqlOutsideLiterals(sql4, transform) {
3742
3862
  break;
3743
3863
  }
3744
3864
  if ((c === "E" || c === "e") && next === "'") {
3745
- const prev = i > 0 ? sql4[i - 1] : "";
3865
+ const prev = i > 0 ? sql5[i - 1] : "";
3746
3866
  if (!/[A-Za-z0-9_]/.test(prev)) {
3747
3867
  emitCode(i);
3748
3868
  state = { kind: "e-single" };
@@ -3761,9 +3881,9 @@ function transformSqlOutsideLiterals(sql4, transform) {
3761
3881
  break;
3762
3882
  }
3763
3883
  if (c === "$") {
3764
- const prev = i > 0 ? sql4[i - 1] : "";
3884
+ const prev = i > 0 ? sql5[i - 1] : "";
3765
3885
  if (!/[A-Za-z0-9_$]/.test(prev)) {
3766
- const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
3886
+ const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql5.slice(i));
3767
3887
  if (tagMatch) {
3768
3888
  emitCode(i);
3769
3889
  state = { kind: "dollar", tag: tagMatch[1] ?? "" };
@@ -3795,8 +3915,8 @@ function transformSqlOutsideLiterals(sql4, transform) {
3795
3915
  break;
3796
3916
  }
3797
3917
  case "e-single": {
3798
- if (c === "\\" && i + 1 < sql4.length) {
3799
- out.push(c, sql4[i + 1]);
3918
+ if (c === "\\" && i + 1 < sql5.length) {
3919
+ out.push(c, sql5[i + 1]);
3800
3920
  i += 2;
3801
3921
  break;
3802
3922
  }
@@ -3818,7 +3938,7 @@ function transformSqlOutsideLiterals(sql4, transform) {
3818
3938
  }
3819
3939
  case "dollar": {
3820
3940
  const closer = `$${state.tag}$`;
3821
- if (sql4.startsWith(closer, i)) {
3941
+ if (sql5.startsWith(closer, i)) {
3822
3942
  out.push(closer);
3823
3943
  state = { kind: "normal" };
3824
3944
  i += closer.length;
@@ -3862,15 +3982,15 @@ function transformSqlOutsideLiterals(sql4, transform) {
3862
3982
  }
3863
3983
  }
3864
3984
  }
3865
- emitCode(sql4.length);
3985
+ emitCode(sql5.length);
3866
3986
  return out.join("");
3867
3987
  }
3868
3988
  function escapeRegExp(value) {
3869
3989
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3870
3990
  }
3871
- function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
3991
+ function sqlReferencesQualifiedRelation(sql5, sourceSchema, sourceName) {
3872
3992
  let referenced = false;
3873
- transformSqlOutsideLiterals(sql4, (segment) => {
3993
+ transformSqlOutsideLiterals(sql5, (segment) => {
3874
3994
  if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
3875
3995
  return segment;
3876
3996
  });
@@ -3881,15 +4001,16 @@ function qualifiedRelationPattern(sourceSchema, sourceName) {
3881
4001
  const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
3882
4002
  return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
3883
4003
  }
3884
- function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
3885
- return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
4004
+ function replaceQualifiedRelation(sql5, sourceSchema, sourceName, boundSchema, boundTable) {
4005
+ return sql5.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
3886
4006
  }
3887
4007
  function generateSnapshotV2(definition, previousId3) {
3888
4008
  const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
3889
4009
  const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
3890
- if ((definition.views?.length ?? 0) > 0) {
4010
+ if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
3891
4011
  const extended = snapshot;
3892
4012
  extended._sequenceOrm = {
4013
+ ...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
3893
4014
  viewDependencies: Object.fromEntries(
3894
4015
  (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
3895
4016
  )
@@ -3907,7 +4028,7 @@ function buildViewColumnV2({
3907
4028
  if (!column.nullable) builder = builder.notNull();
3908
4029
  return builder;
3909
4030
  }
3910
- function buildTable2(schema, table, built, enums, byName) {
4031
+ function buildTable2(schema, table, built, enums, byName, introspection) {
3911
4032
  const primaryKeyColumns = table.primaryKey ?? [];
3912
4033
  if (primaryKeyColumns.length === 0) {
3913
4034
  throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
@@ -3923,13 +4044,22 @@ function buildTable2(schema, table, built, enums, byName) {
3923
4044
  const columns = Object.fromEntries(
3924
4045
  table.columns.map((column) => [
3925
4046
  column.columnName,
3926
- buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
4047
+ buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums, introspection })
3927
4048
  ])
3928
4049
  );
3929
4050
  const drizzleTable = schema.table(table.table, columns, (self) => [
3930
4051
  ...buildCompositePrimaryKey(table, primaryKeyColumns, self),
3931
4052
  ...buildIndexes2(table, self),
3932
- ...buildForeignKeys2(table, self, built, byName)
4053
+ ...buildForeignKeys2(table, self, built, byName),
4054
+ ...(table.foreignKeys ?? []).map((fk) => {
4055
+ const target = fk.target === table.table ? self : built.get(fk.target);
4056
+ if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
4057
+ return pgCore.foreignKey({
4058
+ name: fk.name,
4059
+ columns: fk.columns.map((column) => self[column]),
4060
+ foreignColumns: fk.references.map((column) => target[column])
4061
+ });
4062
+ })
3933
4063
  ]);
3934
4064
  return drizzleTable.enableRLS();
3935
4065
  }
@@ -3946,7 +4076,8 @@ function buildColumnV2({
3946
4076
  column,
3947
4077
  singlePrimaryKey,
3948
4078
  compositeMembers,
3949
- enums
4079
+ enums,
4080
+ introspection
3950
4081
  }) {
3951
4082
  const factory = resolveColumnFactory(table, column, enums);
3952
4083
  let builder = factory(column.columnName);
@@ -3967,7 +4098,7 @@ function buildColumnV2({
3967
4098
  }
3968
4099
  builder = builder.default(drizzleOrm.sql.raw(column.defaultFn));
3969
4100
  } else if (column.default !== void 0 && column.default !== null) {
3970
- builder = builder.default(column.default);
4101
+ builder = introspection && column.type === "jsonb" && typeof column.default === "string" ? builder.default(drizzleOrm.sql.raw(`${quoteLiteral(column.default)}::jsonb`)) : builder.default(column.default);
3971
4102
  }
3972
4103
  return builder;
3973
4104
  }
@@ -4152,7 +4283,8 @@ async function planMigrationV2({
4152
4283
  assertValidViewDependencies(canonical);
4153
4284
  const hints = applyMigrationHintsV2(previous, canonical);
4154
4285
  const snapshot = generateSnapshotV2(canonical, previousId(previous));
4155
- const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
4286
+ const generatedBaseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
4287
+ const baseDdl = orderForeignKeyAdditions(canonical, generatedBaseDdl);
4156
4288
  const previousViews = snapshotViews(hints.adjusted);
4157
4289
  const currentViews = snapshotViews(snapshot);
4158
4290
  const previousDependencies = snapshotViewDependencies(hints.adjusted);
@@ -4189,6 +4321,7 @@ async function planMigrationV2({
4189
4321
  ...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
4190
4322
  ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
4191
4323
  ...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
4324
+ ...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql5) => ({ kind: "ddl", sql: sql5 })),
4192
4325
  ...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
4193
4326
  ...createOrder.map((name) => {
4194
4327
  const view = currentViewDefinitions.get(name);
@@ -4198,6 +4331,29 @@ async function planMigrationV2({
4198
4331
  ];
4199
4332
  return { namespace: canonical.namespace, schema, statements, snapshot };
4200
4333
  }
4334
+ function orderForeignKeyAdditions(canonical, statements) {
4335
+ if (!canonical.tables.some((table) => table.foreignKeys?.length)) return [...statements];
4336
+ const addsForeignKey = (statement) => /^ALTER TABLE[\s\S]* ADD CONSTRAINT[\s\S]* FOREIGN KEY\b/.test(statement);
4337
+ return [...statements.filter((statement) => !addsForeignKey(statement)), ...statements.filter(addsForeignKey)];
4338
+ }
4339
+ function foreignKeyTimingStatements(canonical, previous, baseDdl) {
4340
+ const prior = previous._sequenceOrm?.foreignKeys ?? {};
4341
+ const result = [];
4342
+ for (const table of canonical.tables) {
4343
+ for (const fk of table.foreignKeys ?? []) {
4344
+ const before = prior[`${table.table}.${fk.name}`];
4345
+ const recreated = baseDdl.some((statement) => statement.includes(`ALTER TABLE ${quoteIdentifier(ormSchemaName(canonical.namespace))}.${quoteIdentifier(table.table)} ADD CONSTRAINT ${quoteIdentifier(fk.name)}`));
4346
+ if (fk.deferrable ? !before || before.deferrable !== fk.deferrable || recreated : before?.deferrable && !recreated) {
4347
+ result.push(foreignKeyTimingSql(ormSchemaName(canonical.namespace), table.table, fk.name, fk.deferrable));
4348
+ }
4349
+ }
4350
+ }
4351
+ return result;
4352
+ }
4353
+ function foreignKeyTimingSql(schema, table, name, timing) {
4354
+ const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
4355
+ return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name)} ${clause}`;
4356
+ }
4201
4357
  function validateViewCatalogStatement({
4202
4358
  canonical,
4203
4359
  view,
@@ -4452,7 +4608,63 @@ async function pendingKitStatementsV2({
4452
4608
  canonical,
4453
4609
  db
4454
4610
  }) {
4455
- return pendingStatements({ entities: buildDrizzleEntitiesV2(canonical), db, owned: V2_OWNED_STATEMENT });
4611
+ const { desired, defaultDrift } = await normalizeJsonDefaultsForIntrospection(canonical, db);
4612
+ const entities = buildDrizzleEntitiesV2(desired, { introspection: true });
4613
+ const pending = await pendingStatements({ entities, db, owned: V2_OWNED_STATEMENT });
4614
+ return [...orderForeignKeyAdditions(canonical, pending), ...defaultDrift, ...await pendingForeignKeyTimingStatements(canonical, db)];
4615
+ }
4616
+ async function normalizeJsonDefaultsForIntrospection(canonical, db) {
4617
+ const defaults = canonical.tables.flatMap((table, tableIndex) => table.source ? [] : table.columns.flatMap((column, columnIndex) => column.type === "jsonb" && typeof column.default === "string" ? [{ tableIndex, columnIndex, table: table.table, column: column.columnName, value: column.default }] : []));
4618
+ if (defaults.length === 0) return { desired: canonical, defaultDrift: [] };
4619
+ const array = (values) => drizzleOrm.sql`ARRAY[${drizzleOrm.sql.join(values.map((value) => drizzleOrm.sql`${value}`), drizzleOrm.sql`, `)}]::text[]`;
4620
+ const schema = ormSchemaName(canonical.namespace);
4621
+ const result = await shimExecute(db).execute(drizzleOrm.sql`
4622
+ SELECT desired.ordinal, desired.value::jsonb::text AS normalized,
4623
+ attribute.attnum IS NOT NULL AS column_exists,
4624
+ coalesce(pg_get_expr(ad.adbin, ad.adrelid) = quote_literal(desired.value::jsonb::text) || '::jsonb', false) AS matches
4625
+ FROM unnest(${array(defaults.map((item) => item.table))}, ${array(defaults.map((item) => item.column))}, ${array(defaults.map((item) => item.value))})
4626
+ WITH ORDINALITY AS desired(table_name, column_name, value, ordinal)
4627
+ LEFT JOIN pg_catalog.pg_namespace namespace ON namespace.nspname = ${schema}
4628
+ LEFT JOIN pg_catalog.pg_class relation ON relation.relnamespace = namespace.oid AND relation.relname = desired.table_name
4629
+ LEFT JOIN pg_catalog.pg_attribute attribute ON attribute.attrelid = relation.oid AND attribute.attname = desired.column_name AND NOT attribute.attisdropped
4630
+ LEFT JOIN pg_catalog.pg_attrdef ad ON ad.adrelid = relation.oid AND ad.adnum = attribute.attnum
4631
+ ORDER BY desired.ordinal
4632
+ `);
4633
+ const rows = result.rows;
4634
+ if (!Array.isArray(rows) || rows.length !== defaults.length || rows.some((row, index3) => Number(row.ordinal) !== index3 + 1 || typeof row.normalized !== "string" || typeof row.column_exists !== "boolean" || typeof row.matches !== "boolean")) {
4635
+ throw new Error("invalid JSON default introspection result");
4636
+ }
4637
+ const desired = structuredClone(canonical);
4638
+ const defaultDrift = [];
4639
+ defaults.forEach((item, index3) => {
4640
+ const row = rows[index3];
4641
+ desired.tables[item.tableIndex].columns[item.columnIndex].default = JSON.stringify(JSON.parse(row.normalized));
4642
+ if (row.column_exists && !row.matches) defaultDrift.push(
4643
+ `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(item.table)} ALTER COLUMN ${quoteIdentifier(item.column)} SET DEFAULT ${quoteLiteral(item.value)}::jsonb;`
4644
+ );
4645
+ });
4646
+ return { desired, defaultDrift };
4647
+ }
4648
+ async function pendingForeignKeyTimingStatements(canonical, db) {
4649
+ const keys = canonical.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => ({ table: table.table, fk })));
4650
+ if (keys.length === 0) return [];
4651
+ const schema = ormSchemaName(canonical.namespace);
4652
+ const result = await shimExecute(db).execute(drizzleOrm.sql`
4653
+ SELECT relation.relname AS table_name, constraint_row.conname AS name,
4654
+ constraint_row.condeferrable AS deferrable, constraint_row.condeferred AS deferred
4655
+ FROM pg_catalog.pg_constraint AS constraint_row
4656
+ JOIN pg_catalog.pg_class AS relation ON relation.oid = constraint_row.conrelid
4657
+ JOIN pg_catalog.pg_namespace AS namespace ON namespace.oid = relation.relnamespace
4658
+ WHERE namespace.nspname = ${schema} AND constraint_row.contype = 'f'
4659
+ `);
4660
+ const rows = result.rows;
4661
+ return keys.flatMap(({ table, fk }) => {
4662
+ const actual = rows.find((row) => row.table_name === table && row.name === fk.name);
4663
+ const deferrable = fk.deferrable !== void 0;
4664
+ const deferred = fk.deferrable === "initially-deferred";
4665
+ if (actual ? actual.deferrable === deferrable && actual.deferred === deferred : !deferrable) return [];
4666
+ return [foreignKeyTimingSql(schema, table, fk.name, fk.deferrable)];
4667
+ });
4456
4668
  }
4457
4669
  async function pendingStatements({
4458
4670
  entities,
@@ -4465,11 +4677,31 @@ async function pendingStatements({
4465
4677
  );
4466
4678
  return statementsToExecute.filter((statement) => !owned.test(statement));
4467
4679
  }
4680
+ function correctIntrospectionQuery(query) {
4681
+ if (!drizzleOrm.is(query, drizzleOrm.SQL)) return query;
4682
+ const compiled = new pgCore.PgDialect().sqlToQuery(query);
4683
+ let text4 = compiled.sql;
4684
+ const indexJoin = "pg_constraint con ON con.conindid = idx.indexrelid";
4685
+ if (text4.includes("AS generated_by_constraint") && text4.includes(indexJoin)) {
4686
+ text4 = text4.replace(indexJoin, `${indexJoin} AND con.contype IN ('p', 'u', 'x')`);
4687
+ }
4688
+ const sourceJoin = "LEFT JOIN pg_catalog.pg_attribute att ON att.attnum = ANY (con.conkey)";
4689
+ const targetJoin = "LEFT JOIN pg_catalog.pg_attribute fatt ON fatt.attnum = ANY (con.confkey)";
4690
+ if (text4.includes(sourceJoin) && text4.includes(targetJoin) && text4.includes("AND con.contype IN ('f')")) {
4691
+ text4 = text4.replace(
4692
+ sourceJoin,
4693
+ "JOIN LATERAL unnest(con.conkey, con.confkey) WITH ORDINALITY AS fk_key(source_num, target_num, ordinal) ON true LEFT JOIN pg_catalog.pg_attribute att ON att.attnum = fk_key.source_num"
4694
+ ).replace(targetJoin, "LEFT JOIN pg_catalog.pg_attribute fatt ON fatt.attnum = fk_key.target_num").replace(/;\s*$/, "") + " ORDER BY con.conname, fk_key.ordinal";
4695
+ }
4696
+ if (text4 === compiled.sql) return query;
4697
+ if (compiled.params.length !== 0) throw new Error("parameterized Drizzle catalog query cannot be normalized");
4698
+ return drizzleOrm.sql.raw(text4);
4699
+ }
4468
4700
  function shimExecute(db) {
4469
4701
  const execute = db.execute.bind(db);
4470
4702
  return {
4471
4703
  execute: async (query) => {
4472
- const result = await execute(query);
4704
+ const result = await execute(correctIntrospectionQuery(query));
4473
4705
  if (result && typeof result === "object" && "rows" in result) return result;
4474
4706
  return { rows: result };
4475
4707
  }
@@ -4509,21 +4741,73 @@ async function planMigration({
4509
4741
  const ddl = await diffStatements(hints.adjusted, snapshot);
4510
4742
  const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
4511
4743
  const statements = [
4512
- ...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
4513
- ...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
4514
- ...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
4515
- ...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
4744
+ ...policyDrops.map((sql5) => ({ kind: "security", sql: sql5 })),
4745
+ ...hints.statements.map((sql5) => ({ kind: "hint", sql: sql5 })),
4746
+ ...ddl.map((sql5) => ({ kind: "ddl", sql: sql5.replace(/;\s*$/, "") })),
4747
+ ...securityStatements(definition).map((sql5) => ({ kind: "security", sql: sql5 })),
4516
4748
  // PREPARE each action's executes first (plpgsql bodies don't check columns
4517
4749
  // at CREATE), then create the functions. Both reference the tables the DDL
4518
4750
  // above creates, so they close the plan.
4519
- ...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
4520
- ...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
4751
+ ...actionPrepareChecks(definition).map((sql5) => ({ kind: "function", sql: sql5 })),
4752
+ ...actionFunctionStatements(definition).map((sql5) => ({ kind: "function", sql: sql5 }))
4521
4753
  ];
4522
4754
  return { namespace: definition.namespace, schema, statements, snapshot };
4523
4755
  }
4524
4756
  var compileNamespace = (definition) => planMigration({ definition });
4525
4757
  var previousId2 = (snapshot) => snapshot.id;
4526
- var IR_VERSION = 1;
4758
+ var ORM_MIGRATION_EXPORT_FILE = "orm.migration.json";
4759
+ var ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
4760
+ var hash = zod.z.string().regex(/^sha256:[0-9a-f]{64}$/);
4761
+ var ormMigrationPayloadSchema = zod.z.object({
4762
+ canonical_definition: canonicalDefinitionSchema,
4763
+ documents: zod.z.array(zod.z.object({ path: zod.z.string().min(1).max(2048), contents: zod.z.string().max(1e6) }).strict()).max(5e3),
4764
+ client_manifest: zod.z.object({ ops_hash: hash, operations: zod.z.record(zod.z.string(), hash) }).strict(),
4765
+ sdl: zod.z.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_GENERATED_SDL_BYTES, {
4766
+ message: `sdl exceeds ${MAX_GENERATED_SDL_BYTES} bytes`
4767
+ }),
4768
+ schema_snapshot: zod.z.json(),
4769
+ // The existing ORM committed-migration validator and replay engine remain
4770
+ // authoritative. This envelope hashes the provenance, never authorizes SQL.
4771
+ migration: zod.z.json().optional()
4772
+ }).strict();
4773
+ var ormMigrationExportSchema = zod.z.object({
4774
+ schema_version: zod.z.literal(1),
4775
+ schema_hash: hash,
4776
+ payload: ormMigrationPayloadSchema,
4777
+ checksum: zod.z.string().regex(/^[0-9a-f]{64}$/)
4778
+ }).strict();
4779
+ function checksum(value) {
4780
+ return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
4781
+ }
4782
+ function createOrmMigrationExport({ schemaHash, payload }) {
4783
+ const contents = { schema_version: 1, schema_hash: hash.parse(schemaHash), payload: ormMigrationPayloadSchema.parse(payload) };
4784
+ return { ...contents, checksum: checksum(contents) };
4785
+ }
4786
+ function parseOrmMigrationExport(text4) {
4787
+ if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
4788
+ const exported = ormMigrationExportSchema.parse(JSON.parse(text4));
4789
+ const { checksum: expected, ...contents } = exported;
4790
+ if (checksum(contents) !== expected) throw new Error("ORM migration export checksum mismatch; regenerate and commit it");
4791
+ return exported;
4792
+ }
4793
+ function renderOrmMigrationExport(exported) {
4794
+ const text4 = `${JSON.stringify(exported, null, 2)}
4795
+ `;
4796
+ if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
4797
+ return text4;
4798
+ }
4799
+ var IR_VERSION = 2;
4800
+ var LEGACY_IR_VERSION = 1;
4801
+ var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
4802
+ var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
4803
+ function requiredArtifactIrVersion(operations) {
4804
+ let required = LEGACY_IR_VERSION;
4805
+ for (const operation of operations) {
4806
+ const { irVersion } = operationVersionSchema.parse(operation);
4807
+ if (irVersion > required) required = irVersion;
4808
+ }
4809
+ return required;
4810
+ }
4527
4811
  var CLAIMS_CONTRACT = {
4528
4812
  sub: "string",
4529
4813
  org: "string?",
@@ -4650,6 +4934,33 @@ var aggregateRootSchema = zod.z.object({
4650
4934
  nodes: selectionIrSchema.optional(),
4651
4935
  includeIf: includeIfSchema.optional()
4652
4936
  });
4937
+ var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
4938
+ var groupByKeyIrSchema = zod.z.object({
4939
+ column: zod.z.string(),
4940
+ bucket: zod.z.enum(DATE_BUCKETS).optional()
4941
+ });
4942
+ var aggregateGroupsRootSchema = zod.z.object({
4943
+ kind: zod.z.literal("aggregateGroups"),
4944
+ alias: zod.z.string(),
4945
+ table: zod.z.string(),
4946
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
4947
+ where: predIrSchema,
4948
+ groupBy: zod.z.array(groupByKeyIrSchema).min(1),
4949
+ /**
4950
+ * The `key { … }` projection: which group columns appear in each entry's key
4951
+ * object, in selection order. A subset of `groupBy` (the compiler rejects a
4952
+ * key field that is not a group key — it would be indeterminate within the
4953
+ * group). Empty when the document selects only `aggregate`.
4954
+ */
4955
+ key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
4956
+ count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
4957
+ sum: zod.z.array(zod.z.string()).optional(),
4958
+ avg: zod.z.array(zod.z.string()).optional(),
4959
+ /** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
4960
+ limit: limitIrSchema,
4961
+ offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
4962
+ includeIf: includeIfSchema.optional()
4963
+ });
4653
4964
  var writeValuesSchema = zod.z.union([
4654
4965
  zod.z.object({ slot: zod.z.string() }),
4655
4966
  zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
@@ -4695,12 +5006,15 @@ var mutationRootSchema = zod.z.object({
4695
5006
  insertCheck: predIrSchema.optional(),
4696
5007
  returning: selectionIrSchema.nullable(),
4697
5008
  affectedRows: zod.z.boolean().default(false),
5009
+ /** v2: abort the transaction when the actual write count differs, regardless of selection. */
5010
+ assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
4698
5011
  includeIf: includeIfSchema.optional()
4699
5012
  });
4700
5013
  var rootIrSchema = zod.z.discriminatedUnion("kind", [
4701
5014
  selectRootSchema,
4702
5015
  byPkRootSchema,
4703
5016
  aggregateRootSchema,
5017
+ aggregateGroupsRootSchema,
4704
5018
  mutationRootSchema
4705
5019
  ]);
4706
5020
  var INPUT_REF_KINDS = [
@@ -4730,7 +5044,7 @@ var variableIrSchema = zod.z.object({
4730
5044
  defaultValue: variableDefaultIrSchema.optional()
4731
5045
  });
4732
5046
  var compiledOperationSchema = zod.z.object({
4733
- irVersion: zod.z.literal(IR_VERSION),
5047
+ irVersion: irVersionSchema,
4734
5048
  name: zod.z.string(),
4735
5049
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
4736
5050
  kind: zod.z.enum(["query", "mutation"]),
@@ -4742,6 +5056,13 @@ var compiledOperationSchema = zod.z.object({
4742
5056
  /** Every table the op touches — data-token scoping + audit. */
4743
5057
  tableSet: zod.z.array(zod.z.string()).min(1),
4744
5058
  roots: zod.z.array(rootIrSchema).min(1)
5059
+ }).superRefine((operation, ctx) => {
5060
+ for (const [index3, root] of operation.roots.entries()) {
5061
+ if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
5062
+ if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
5063
+ ctx.addIssue({ code: "custom", path: ["roots", index3], message: "affected-row assertions require unconditional mutation roots and IR v2" });
5064
+ }
5065
+ }
4745
5066
  });
4746
5067
  var tableMetaIrSchema = zod.z.object({
4747
5068
  table: zod.z.string(),
@@ -4778,18 +5099,29 @@ var tableMetaIrSchema = zod.z.object({
4778
5099
  insertable: zod.z.array(zod.z.string()),
4779
5100
  settable: zod.z.array(zod.z.string()),
4780
5101
  appendable: zod.z.array(zod.z.string()),
5102
+ /**
5103
+ * Logical columns stamped with Postgres `now()` on UPDATE (canonical
5104
+ * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
5105
+ */
5106
+ touchOnUpdate: zod.z.array(zod.z.string()).optional(),
4781
5107
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
4782
5108
  /** Depth/node caps for dynamic filter trees on this table. */
4783
5109
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
4784
5110
  dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
4785
5111
  });
4786
5112
  var compiledArtifactSchema = zod.z.object({
4787
- irVersion: zod.z.literal(IR_VERSION),
5113
+ irVersion: irVersionSchema,
4788
5114
  namespace: zod.z.string(),
4789
5115
  schemaHash: zod.z.string(),
4790
5116
  opsHash: zod.z.string(),
4791
5117
  tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
4792
5118
  operations: zod.z.record(zod.z.string(), compiledOperationSchema)
5119
+ }).superRefine((artifact, ctx) => {
5120
+ for (const [name, operation] of Object.entries(artifact.operations)) {
5121
+ if (operation.irVersion > artifact.irVersion) {
5122
+ ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
5123
+ }
5124
+ }
4793
5125
  });
4794
5126
  var IrVersionError = class extends Error {
4795
5127
  constructor(found) {
@@ -4800,7 +5132,7 @@ var IrVersionError = class extends Error {
4800
5132
  function normalizeIr(raw) {
4801
5133
  if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
4802
5134
  const version = raw.irVersion;
4803
- if (version !== IR_VERSION) throw new IrVersionError(version);
5135
+ if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
4804
5136
  return compiledArtifactSchema.parse(raw);
4805
5137
  }
4806
5138
  function indexByHash(artifact) {
@@ -5116,12 +5448,12 @@ async function writeMigration({
5116
5448
  const tip = workspace.journal.entries.at(-1);
5117
5449
  const ordinal = (tip?.ordinal ?? 0) + 1;
5118
5450
  const name = migrationName(ordinal, contentHash);
5119
- const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
5451
+ const sql5 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
5120
5452
  const metaDir = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
5121
5453
  await promises.mkdir(metaDir, { recursive: true });
5122
5454
  const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
5123
5455
  `;
5124
- await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
5456
+ await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql5);
5125
5457
  await promises.writeFile(path.join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
5126
5458
  const journal = {
5127
5459
  ...workspace.journal,
@@ -5132,7 +5464,7 @@ async function writeMigration({
5132
5464
  name,
5133
5465
  content_hash: contentHash,
5134
5466
  previous_content_hash: tip?.content_hash ?? null,
5135
- checksum: checksumOf(sql4),
5467
+ checksum: checksumOf(sql5),
5136
5468
  snapshot_checksum: checksumOf(snapshotText),
5137
5469
  destructive
5138
5470
  }
@@ -5247,8 +5579,8 @@ async function checkWorkspace({
5247
5579
  let previousId3;
5248
5580
  let previousHash = null;
5249
5581
  for (const entry of workspace.journal.entries) {
5250
- const sql4 = await readMigrationSql(dir, entry);
5251
- if (checksumOf(sql4) !== entry.checksum) {
5582
+ const sql5 = await readMigrationSql(dir, entry);
5583
+ if (checksumOf(sql5) !== entry.checksum) {
5252
5584
  log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
5253
5585
  return 1;
5254
5586
  }
@@ -5288,7 +5620,7 @@ async function checkWorkspace({
5288
5620
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
5289
5621
  return 0;
5290
5622
  }
5291
- var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
5623
+ var firstLine = (sql5) => sql5.split("\n")[0] ?? sql5;
5292
5624
 
5293
5625
  // src/migrations/from-yaml.ts
5294
5626
  function convertV1DefinitionToCanonical(definition) {
@@ -5394,7 +5726,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
5394
5726
  throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
5395
5727
  }
5396
5728
  const name = migrationName(ordinal, newHash);
5397
- const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
5729
+ const sql5 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
5398
5730
  const snapshotText = `${JSON.stringify(snapshot, null, 2)}
5399
5731
  `;
5400
5732
  const entry = {
@@ -5402,11 +5734,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
5402
5734
  name,
5403
5735
  content_hash: newHash,
5404
5736
  previous_content_hash: previousHash,
5405
- checksum: checksumOf(sql4),
5737
+ checksum: checksumOf(sql5),
5406
5738
  snapshot_checksum: checksumOf(snapshotText),
5407
5739
  destructive: false
5408
5740
  };
5409
- return { name, entry, sql: sql4, snapshotText };
5741
+ return { name, entry, sql: sql5, snapshotText };
5410
5742
  }
5411
5743
  function renderRebaseMigrationSql({
5412
5744
  name,
@@ -5536,10 +5868,12 @@ exports.CLAIM_KEY_PATTERN = CLAIM_KEY_PATTERN;
5536
5868
  exports.CMP_OPS = CMP_OPS;
5537
5869
  exports.COLUMN_BRAND = COLUMN_BRAND;
5538
5870
  exports.COLUMN_DEFAULT_FNS = COLUMN_DEFAULT_FNS;
5871
+ exports.COLUMN_IDENTIFIER_PATTERN = COLUMN_IDENTIFIER_PATTERN;
5539
5872
  exports.COLUMN_TYPES = COLUMN_TYPES2;
5540
5873
  exports.COMPARE_OPS = COMPARE_OPS;
5541
5874
  exports.CONSUMER_ROLE_SUFFIX = CONSUMER_ROLE_SUFFIX;
5542
5875
  exports.ColumnBuilder = ColumnBuilder;
5876
+ exports.DATE_BUCKETS = DATE_BUCKETS;
5543
5877
  exports.DEFINITION_FILE_EXTENSIONS = DEFINITION_FILE_EXTENSIONS;
5544
5878
  exports.ENUM_BRAND = ENUM_BRAND;
5545
5879
  exports.HASH_DOMAIN = HASH_DOMAIN;
@@ -5548,8 +5882,10 @@ exports.IDENTIFIER_RE = IDENTIFIER_RE;
5548
5882
  exports.INPUT_REF_KINDS = INPUT_REF_KINDS;
5549
5883
  exports.IR_VERSION = IR_VERSION;
5550
5884
  exports.IrVersionError = IrVersionError;
5885
+ exports.LEGACY_IR_VERSION = LEGACY_IR_VERSION;
5551
5886
  exports.LINK_BRAND = LINK_BRAND;
5552
5887
  exports.MAX_DEFINITION_FILE_BYTES = MAX_DEFINITION_FILE_BYTES;
5888
+ exports.MAX_GENERATED_SDL_BYTES = MAX_GENERATED_SDL_BYTES;
5553
5889
  exports.MAX_IDENTIFIER_LENGTH = MAX_IDENTIFIER_LENGTH;
5554
5890
  exports.MIGRATIONS_DIRNAME = MIGRATIONS_DIRNAME;
5555
5891
  exports.NAMESPACE_CONFIG_BRAND = NAMESPACE_CONFIG_BRAND;
@@ -5557,6 +5893,9 @@ exports.NAMESPACE_CONFIG_FILE = NAMESPACE_CONFIG_FILE;
5557
5893
  exports.NAMESPACE_CONFIG_FILENAMES = NAMESPACE_CONFIG_FILENAMES;
5558
5894
  exports.NAMESPACE_PATTERN = NAMESPACE_PATTERN;
5559
5895
  exports.ORM_DATA_TOKEN_HEADER = ORM_DATA_TOKEN_HEADER;
5896
+ exports.ORM_MIGRATION_EXPORT_FILE = ORM_MIGRATION_EXPORT_FILE;
5897
+ exports.ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_MIGRATION_EXPORT_MAX_BYTES;
5898
+ exports.ORM_REGISTRATION_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
5560
5899
  exports.ORM_SCHEMA_PREFIX = ORM_SCHEMA_PREFIX;
5561
5900
  exports.ORM_SERVICE_DATA_TOKEN_HEADER = ORM_SERVICE_DATA_TOKEN_HEADER;
5562
5901
  exports.ORM_TO_PIPELINE_TYPE = ORM_TO_PIPELINE_TYPE;
@@ -5621,6 +5960,7 @@ exports.computeDefinitionHash = computeDefinitionHash;
5621
5960
  exports.computeSyncedContractHash = computeSyncedContractHash;
5622
5961
  exports.consumerRoleName = consumerRoleName;
5623
5962
  exports.convertV1DefinitionToCanonical = convertV1DefinitionToCanonical;
5963
+ exports.createOrmMigrationExport = createOrmMigrationExport;
5624
5964
  exports.declaredCapabilityKeys = declaredCapabilityKeys;
5625
5965
  exports.defineEnum = defineEnum;
5626
5966
  exports.defineNamespace = defineNamespace;
@@ -5662,11 +6002,13 @@ exports.normalizeCanonical = normalizeCanonical;
5662
6002
  exports.normalizeIr = normalizeIr;
5663
6003
  exports.normalizeSnapshot = normalizeSnapshot;
5664
6004
  exports.orderIrSchema = orderIrSchema;
6005
+ exports.ormMigrationExportSchema = ormMigrationExportSchema;
5665
6006
  exports.ormSchemaName = ormSchemaName;
5666
6007
  exports.overlayWinsUnion = overlayWinsUnion;
5667
6008
  exports.overrideRowMutation = overrideRowMutation;
5668
6009
  exports.parseCanonicalDefinition = parseCanonicalDefinition;
5669
6010
  exports.parseDefinitionSources = parseDefinitionSources;
6011
+ exports.parseOrmMigrationExport = parseOrmMigrationExport;
5670
6012
  exports.parsePredicate = parsePredicate;
5671
6013
  exports.parseTableFile = parseTableFile;
5672
6014
  exports.parsedPredicateSchema = parsedPredicateSchema;
@@ -5687,6 +6029,8 @@ exports.rebaseViolations = rebaseViolations;
5687
6029
  exports.referencedCapabilities = referencedCapabilities;
5688
6030
  exports.referencedColumns = referencedColumns;
5689
6031
  exports.renderMigrationSql = renderMigrationSql;
6032
+ exports.renderOrmMigrationExport = renderOrmMigrationExport;
6033
+ exports.requiredArtifactIrVersion = requiredArtifactIrVersion;
5690
6034
  exports.revertOverlayMutation = revertOverlayMutation;
5691
6035
  exports.rewriteSyncedSourceRelationsInViewQuery = rewriteSyncedSourceRelationsInViewQuery;
5692
6036
  exports.roleExpression = roleExpression;