@dbsp/adapter-pgsql 1.10.2 → 1.11.2

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.
package/dist/index.js CHANGED
@@ -94,8 +94,373 @@ var init_naming_plugin = __esm({
94
94
  }
95
95
  });
96
96
 
97
+ // src/db-type.ts
98
+ function splitDbType(input) {
99
+ const trimmed = input.trim();
100
+ let rest = trimmed;
101
+ let isArray = false;
102
+ if (rest.endsWith("[]")) {
103
+ isArray = true;
104
+ rest = rest.slice(0, -2).trimEnd();
105
+ }
106
+ const lowerRest = rest.toLowerCase();
107
+ for (const suffix of TEMPORAL_TZ_SUFFIXES) {
108
+ if (!lowerRest.endsWith(suffix)) continue;
109
+ const beforeSuffix = rest.slice(0, -suffix.length).trimEnd();
110
+ const temporal = splitTrailingModifier(beforeSuffix);
111
+ const temporalBase = normalizeBase(temporal.base).toLowerCase();
112
+ if (temporalBase === "timestamp" || temporalBase === "time") {
113
+ const base2 = normalizeBase(`${temporal.base}${suffix}`);
114
+ return {
115
+ base: base2,
116
+ modifier: temporal.modifier,
117
+ isArray,
118
+ schemaQualified: isSchemaQualified(base2)
119
+ };
120
+ }
121
+ }
122
+ const split = splitTrailingModifier(rest);
123
+ const base = normalizeBase(split.base);
124
+ return {
125
+ base,
126
+ modifier: split.modifier,
127
+ isArray,
128
+ schemaQualified: isSchemaQualified(base)
129
+ };
130
+ }
131
+ function splitTrailingModifier(input) {
132
+ const match = input.match(/^(.*)\(([^()]*)\)$/);
133
+ if (!match) return { base: input.trim(), modifier: void 0 };
134
+ return {
135
+ base: (match[1] ?? "").trimEnd(),
136
+ modifier: (match[2] ?? "").trim()
137
+ };
138
+ }
139
+ function normalizeBase(base) {
140
+ const trimmed = base.trim();
141
+ return trimmed.includes('"') ? trimmed : trimmed.replace(/\s+/g, " ");
142
+ }
143
+ function isSchemaQualified(base) {
144
+ return QUALIFIED_IDENTIFIER_RE.test(base);
145
+ }
146
+ function canonicalizeDbType(type) {
147
+ const split = splitDbType(type);
148
+ const base = normalizeBase(split.base);
149
+ const canonicalBase = base.includes('"') ? base : TYPE_ALIASES.get(base.toLowerCase()) ?? base.toLowerCase();
150
+ const modifier = split.modifier === void 0 ? "" : `(${split.modifier.replace(/\s*,\s*/g, ",").trim()})`;
151
+ const arraySuffix = split.isArray ? "[]" : "";
152
+ return `${canonicalBase}${modifier}${arraySuffix}`;
153
+ }
154
+ function dbTypeCastTarget(originalDbType) {
155
+ const split = splitDbType(originalDbType);
156
+ const base = normalizeBase(split.base).toLowerCase();
157
+ const hasModifier = split.modifier !== void 0;
158
+ const arraySuffix = split.isArray ? "[]" : "";
159
+ if (base === "varchar" || base === "character varying") {
160
+ return hasModifier ? `varchar${arraySuffix}` : originalDbType;
161
+ }
162
+ if (base === "char" || base === "character" || base === "bpchar") {
163
+ return `text${arraySuffix}`;
164
+ }
165
+ if (base === "bit" || base === "bit varying" || base === "varbit") {
166
+ return hasModifier ? `bit varying${arraySuffix}` : originalDbType;
167
+ }
168
+ if (base === "numeric" || base === "decimal") {
169
+ return hasModifier ? `numeric${arraySuffix}` : originalDbType;
170
+ }
171
+ if (base === "timestamp with time zone" || base === "timestamptz") {
172
+ return hasModifier ? `timestamp with time zone${arraySuffix}` : originalDbType;
173
+ }
174
+ if (base === "timestamp without time zone") {
175
+ return hasModifier ? `timestamp without time zone${arraySuffix}` : originalDbType;
176
+ }
177
+ if (base === "timestamp") {
178
+ return hasModifier ? `timestamp without time zone${arraySuffix}` : originalDbType;
179
+ }
180
+ if (base === "time with time zone" || base === "timetz") {
181
+ return hasModifier ? `time with time zone${arraySuffix}` : originalDbType;
182
+ }
183
+ if (base === "time without time zone") {
184
+ return hasModifier ? `time without time zone${arraySuffix}` : originalDbType;
185
+ }
186
+ if (base === "time") {
187
+ return hasModifier ? `time${arraySuffix}` : originalDbType;
188
+ }
189
+ if (INTERVAL_BASE_RE.test(base)) {
190
+ return hasModifier ? `${base}${arraySuffix}` : originalDbType;
191
+ }
192
+ return originalDbType;
193
+ }
194
+ function dbTypesEqual(a, b) {
195
+ return canonicalizeDbType(a) === canonicalizeDbType(b);
196
+ }
197
+ function isPgBuiltInTypeName(type) {
198
+ const split = splitDbType(type);
199
+ const base = normalizeBase(split.base);
200
+ if (base.includes('"') || split.schemaQualified) return false;
201
+ const baseLower = base.toLowerCase();
202
+ return MULTIWORD_BASE_TYPES.has(baseLower) || INTERVAL_BASE_RE.test(base) || BUILTIN_BASE_TYPES.has(baseLower);
203
+ }
204
+ function quoteTypeIdentifier(rawIdentifier) {
205
+ if (SAFE_LOWERCASE_IDENTIFIER_RE.test(rawIdentifier)) return rawIdentifier;
206
+ return `"${rawIdentifier.replace(/"/g, '""')}"`;
207
+ }
208
+ function unquoteIdentifier(id) {
209
+ const t = id.trim();
210
+ if (t.length >= 2 && t.startsWith('"') && t.endsWith('"')) {
211
+ return t.slice(1, -1).replace(/""/g, '"');
212
+ }
213
+ return t;
214
+ }
215
+ function enumReferenceKind(originalDbType, typname) {
216
+ const split = splitDbType(originalDbType);
217
+ if (split.schemaQualified) return null;
218
+ if (unquoteIdentifier(normalizeBase(split.base)) !== typname) return null;
219
+ return split.isArray ? "array" : "scalar";
220
+ }
221
+ function validateDbType(type) {
222
+ if (typeof type !== "string") {
223
+ throw new Error(
224
+ `Unsafe database type name: expected a string, received ${typeof type}`
225
+ );
226
+ }
227
+ if (!isStructurallyValidDbType(type)) {
228
+ throw new Error(
229
+ `Unsafe database type name: "${type}". Must be a structurally valid PostgreSQL type name.`
230
+ );
231
+ }
232
+ return type;
233
+ }
234
+ function isStructurallyValidDbType(type) {
235
+ const trimmed = type.trim();
236
+ if (trimmed.length === 0 || trimmed !== type) return false;
237
+ if (/[;]|--|\/\*|\*\//.test(trimmed)) return false;
238
+ if (trimmed.endsWith("[][]")) return false;
239
+ if (hasForbiddenTypeKeyword(trimmed)) return false;
240
+ const split = splitDbType(trimmed);
241
+ const base = split.base;
242
+ const baseLower = base.toLowerCase();
243
+ if (split.modifier !== void 0 && !isSafeModifier(split.modifier, baseLower)) {
244
+ return false;
245
+ }
246
+ if (MULTIWORD_BASE_TYPES.has(baseLower)) {
247
+ return true;
248
+ }
249
+ if (INTERVAL_BASE_RE.test(base)) {
250
+ return true;
251
+ }
252
+ return IDENTIFIER_RE.test(base) || split.schemaQualified;
253
+ }
254
+ function isSafeModifier(modifier, baseLower) {
255
+ const structurallySafe = modifier.length > 0 && !/[();]|--|\/\*|\*\//.test(modifier) && !MODIFIER_OPERATOR_RE.test(modifier) && MODIFIER_CONTENT_RE.test(modifier) && !hasForbiddenTypeKeyword(modifier);
256
+ if (!structurallySafe) return false;
257
+ if (PRECISION_SCALE_BUILTINS.has(baseLower)) {
258
+ return /^\d+(\s*,\s*-?\d+)?$/.test(modifier);
259
+ }
260
+ if (SINGLE_INT_MODIFIER_BUILTINS.has(baseLower) || INTERVAL_BASE_RE.test(baseLower)) {
261
+ return /^\d+$/.test(modifier.trim());
262
+ }
263
+ if (BUILTIN_BASE_TYPES.has(baseLower) || MULTIWORD_BASE_TYPES.has(baseLower)) {
264
+ return false;
265
+ }
266
+ return true;
267
+ }
268
+ function hasForbiddenTypeKeyword(type) {
269
+ const withoutQuotedIdentifiers = type.replace(QUOTED_IDENTIFIER_RE, '""');
270
+ return /\bnot\s+null\b|\b(default|references|check|primary|unique|constraint|select|insert|update|delete|drop|alter|create|truncate|grant|revoke|copy)\b/i.test(
271
+ withoutQuotedIdentifiers
272
+ );
273
+ }
274
+ var TEMPORAL_TZ_SUFFIXES, SAFE_LOWERCASE_IDENTIFIER_RE, MULTIWORD_BASE_TYPES, TYPE_ALIASES, BUILTIN_BASE_TYPES, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER, IDENTIFIER_RE, QUALIFIED_IDENTIFIER_RE, QUOTED_IDENTIFIER_RE, INTERVAL_FIELD, INTERVAL_BASE_RE, MODIFIER_CONTENT_RE, MODIFIER_OPERATOR_RE, SINGLE_INT_MODIFIER_BUILTINS, PRECISION_SCALE_BUILTINS;
275
+ var init_db_type = __esm({
276
+ "src/db-type.ts"() {
277
+ "use strict";
278
+ TEMPORAL_TZ_SUFFIXES = [" without time zone", " with time zone"];
279
+ SAFE_LOWERCASE_IDENTIFIER_RE = /^[a-z_][a-z0-9_]*$/;
280
+ MULTIWORD_BASE_TYPES = /* @__PURE__ */ new Set([
281
+ "timestamp with time zone",
282
+ "timestamp without time zone",
283
+ "time with time zone",
284
+ "time without time zone",
285
+ "double precision",
286
+ "character varying",
287
+ "bit varying"
288
+ ]);
289
+ TYPE_ALIASES = /* @__PURE__ */ new Map([
290
+ ["character varying", "varchar"],
291
+ ["character", "char"],
292
+ ["bpchar", "char"],
293
+ ["bit varying", "varbit"],
294
+ ["timestamp with time zone", "timestamptz"],
295
+ ["timestamp without time zone", "timestamp"],
296
+ ["time with time zone", "timetz"],
297
+ ["time without time zone", "time"],
298
+ ["int", "integer"],
299
+ ["int4", "integer"],
300
+ ["int8", "bigint"],
301
+ ["int2", "smallint"],
302
+ ["float", "double precision"],
303
+ ["float8", "double precision"],
304
+ ["float4", "real"],
305
+ ["bool", "boolean"],
306
+ ["decimal", "numeric"],
307
+ ["serial", "integer"],
308
+ ["serial4", "integer"],
309
+ ["bigserial", "bigint"],
310
+ ["serial8", "bigint"],
311
+ ["smallserial", "smallint"],
312
+ ["serial2", "smallint"]
313
+ ]);
314
+ BUILTIN_BASE_TYPES = /* @__PURE__ */ new Set([
315
+ ...TYPE_ALIASES.keys(),
316
+ ...TYPE_ALIASES.values(),
317
+ "bigint",
318
+ "bit",
319
+ "boolean",
320
+ "box",
321
+ "bytea",
322
+ "char",
323
+ "cidr",
324
+ "circle",
325
+ "date",
326
+ "float",
327
+ "inet",
328
+ "integer",
329
+ "interval",
330
+ "json",
331
+ "jsonb",
332
+ "line",
333
+ "lseg",
334
+ "macaddr",
335
+ "macaddr8",
336
+ "money",
337
+ "numeric",
338
+ "path",
339
+ "pg_lsn",
340
+ "point",
341
+ "polygon",
342
+ "real",
343
+ "text",
344
+ "time",
345
+ "timestamp",
346
+ "timetz",
347
+ "timestamptz",
348
+ "tsquery",
349
+ "tsvector",
350
+ "txid_snapshot",
351
+ "uuid",
352
+ "varbit",
353
+ "varchar",
354
+ "xml",
355
+ "jsonpath",
356
+ "name",
357
+ "oid",
358
+ "regclass",
359
+ "regtype",
360
+ "regrole",
361
+ "regnamespace",
362
+ "regproc",
363
+ "regprocedure",
364
+ "xid",
365
+ "xid8",
366
+ "cid",
367
+ "tid",
368
+ "daterange",
369
+ "int4range",
370
+ "int8range",
371
+ "numrange",
372
+ "tsrange",
373
+ "tstzrange",
374
+ "datemultirange",
375
+ "int4multirange",
376
+ "int8multirange",
377
+ "nummultirange",
378
+ "tsmultirange",
379
+ "tstzmultirange"
380
+ ]);
381
+ UNQUOTED_IDENTIFIER = "[A-Za-z_][A-Za-z0-9_]*";
382
+ QUOTED_IDENTIFIER = '"(?:[^"]|"")+"';
383
+ IDENTIFIER_RE = new RegExp(
384
+ `^(?:${UNQUOTED_IDENTIFIER}|${QUOTED_IDENTIFIER})$`
385
+ );
386
+ QUALIFIED_IDENTIFIER_RE = new RegExp(
387
+ `^(?:${UNQUOTED_IDENTIFIER}|${QUOTED_IDENTIFIER})\\.(?:${UNQUOTED_IDENTIFIER}|${QUOTED_IDENTIFIER})$`
388
+ );
389
+ QUOTED_IDENTIFIER_RE = new RegExp(QUOTED_IDENTIFIER, "g");
390
+ INTERVAL_FIELD = "(?:year|month|day|hour|minute|second)";
391
+ INTERVAL_BASE_RE = new RegExp(
392
+ `^interval(?: ${INTERVAL_FIELD}(?: to ${INTERVAL_FIELD})?)?$`,
393
+ "i"
394
+ );
395
+ MODIFIER_CONTENT_RE = /^[A-Za-z0-9_,.\- ]+$/;
396
+ MODIFIER_OPERATOR_RE = /[=<>+*/%|&!@#^~]/;
397
+ SINGLE_INT_MODIFIER_BUILTINS = /* @__PURE__ */ new Set([
398
+ "varchar",
399
+ "character varying",
400
+ "char",
401
+ "character",
402
+ "bpchar",
403
+ // float(p): PostgreSQL treats float(1..24) as real, float(25..53) as double
404
+ // precision — a single-integer mantissa precision.
405
+ "float",
406
+ "bit",
407
+ "bit varying",
408
+ "varbit",
409
+ "timestamp",
410
+ "timestamptz",
411
+ "timestamp with time zone",
412
+ "timestamp without time zone",
413
+ "time",
414
+ "timetz",
415
+ "time with time zone",
416
+ "time without time zone",
417
+ "interval"
418
+ ]);
419
+ PRECISION_SCALE_BUILTINS = /* @__PURE__ */ new Set(["numeric", "decimal"]);
420
+ }
421
+ });
422
+
97
423
  // src/validate.ts
424
+ function assertString(value, context) {
425
+ if (typeof value !== "string") {
426
+ throw new Error(`${context}: expected a string, received ${typeof value}`);
427
+ }
428
+ }
429
+ function assertNumericLiteral(value, context) {
430
+ if (typeof value !== "number" && typeof value !== "string") {
431
+ throw new Error(
432
+ `${context}: expected a finite number or numeric string, received ${typeof value}`
433
+ );
434
+ }
435
+ const literal4 = typeof value === "number" ? String(value) : value;
436
+ if (typeof value === "number" && !Number.isFinite(value) || !SAFE_DECIMAL_LITERAL.test(literal4)) {
437
+ throw new Error(`${context}: unsafe numeric literal "${literal4}"`);
438
+ }
439
+ return literal4;
440
+ }
441
+ function formatStorageParameterValue(value, context) {
442
+ if (typeof value !== "number" && typeof value !== "string") {
443
+ throw new Error(
444
+ `${context}: expected a string or number, received ${typeof value}`
445
+ );
446
+ }
447
+ if (typeof value === "number") {
448
+ return assertNumericLiteral(value, context);
449
+ }
450
+ if (value.includes("\0")) {
451
+ throw new Error(`${context}: contains null byte`);
452
+ }
453
+ if (SAFE_DECIMAL_LITERAL.test(value) || SAFE_STORAGE_BOOLEAN_LITERAL.test(value) || SAFE_SIMPLE_IDENTIFIER_VALUE.test(value) && !isReservedKeyword(value) && value.toLowerCase() !== "null") {
454
+ return value;
455
+ }
456
+ return `'${value.replace(/'/g, "''")}'`;
457
+ }
98
458
  function validateIdentifier(value, type) {
459
+ if (typeof value !== "string") {
460
+ throw new Error(
461
+ `Invalid ${type} identifier: expected a string, received ${typeof value}`
462
+ );
463
+ }
99
464
  if (!value || value.length === 0) {
100
465
  throw new InvalidIdentifierError(value, type, "cannot be empty");
101
466
  }
@@ -171,7 +536,12 @@ function validateIdentifiers(identifiers) {
171
536
  function sanitizeForDisplay(value) {
172
537
  return value.replace(/[\x00-\x1f\x7f]/g, "?").slice(0, 100);
173
538
  }
539
+ function sanitizeCommentText(value) {
540
+ assertString(value, "SQL comment text");
541
+ return value.replace(/[\x00-\x1f\x7f]/g, "");
542
+ }
174
543
  function validateExtensionName(name, context = "extension") {
544
+ assertString(name, `Invalid ${context} identifier`);
175
545
  if (!name || name.length === 0) {
176
546
  throw new InvalidIdentifierError(name, context, "cannot be empty");
177
547
  }
@@ -257,6 +627,7 @@ function validateExtensionName(name, context = "extension") {
257
627
  }
258
628
  }
259
629
  function validateCollationName(name, context = "collation") {
630
+ assertString(name, `Invalid ${context} identifier`);
260
631
  if (!name || name.length === 0) {
261
632
  throw new InvalidIdentifierError(name, context, "cannot be empty");
262
633
  }
@@ -349,24 +720,113 @@ function validateCollationName(name, context = "collation") {
349
720
  }
350
721
  }
351
722
  function validateSqlExpression(sql, context) {
723
+ assertString(sql, `Unsafe SQL expression in ${context}`);
352
724
  if (/[;]|--|\/\*|\*\/|\$\$|\\/.test(sql)) {
353
725
  throw new Error(
354
726
  `Unsafe SQL expression in ${context}: contains forbidden characters (;, --, /*, */, "$$" (dollar-quoted strings), \\). Value: "${sql}"`
355
727
  );
356
728
  }
357
729
  }
358
- function validateDbTypeName(type) {
359
- if (!SAFE_TYPE_PATTERN.test(type)) {
730
+ function validateCheckExpression(sql, context) {
731
+ if (typeof sql !== "string") {
360
732
  throw new Error(
361
- `Unsafe database type name: "${type}". Must match PostgreSQL type name rules.`
733
+ `Unsafe SQL expression in ${context}: expected a string, received ${typeof sql}`
734
+ );
735
+ }
736
+ let i = 0;
737
+ let singleQuoted = false;
738
+ let dollarQuoteDelimiter;
739
+ while (i < sql.length) {
740
+ if (singleQuoted) {
741
+ if (sql[i] === "'") {
742
+ if (sql[i + 1] === "'") {
743
+ i += 2;
744
+ continue;
745
+ }
746
+ singleQuoted = false;
747
+ }
748
+ i++;
749
+ continue;
750
+ }
751
+ if (dollarQuoteDelimiter) {
752
+ if (sql.startsWith(dollarQuoteDelimiter, i)) {
753
+ i += dollarQuoteDelimiter.length;
754
+ dollarQuoteDelimiter = void 0;
755
+ continue;
756
+ }
757
+ i++;
758
+ continue;
759
+ }
760
+ if (sql[i] === "'") {
761
+ singleQuoted = true;
762
+ i++;
763
+ continue;
764
+ }
765
+ const dollarDelimiter = readDollarQuoteDelimiter(sql, i);
766
+ if (dollarDelimiter) {
767
+ dollarQuoteDelimiter = dollarDelimiter;
768
+ i += dollarDelimiter.length;
769
+ continue;
770
+ }
771
+ const forbiddenToken = forbiddenCheckTokenAt(sql, i);
772
+ if (forbiddenToken) {
773
+ throw new Error(
774
+ `Unsafe SQL expression in ${context}: contains forbidden token "${forbiddenToken}" outside string literal. Value: "${sql}"`
775
+ );
776
+ }
777
+ i++;
778
+ }
779
+ if (singleQuoted) {
780
+ throw new Error(
781
+ `Unsafe SQL expression in ${context}: unterminated single-quoted string literal. Value: "${sql}"`
782
+ );
783
+ }
784
+ if (dollarQuoteDelimiter) {
785
+ throw new Error(
786
+ `Unsafe SQL expression in ${context}: unterminated dollar-quoted string literal ${dollarQuoteDelimiter}. Value: "${sql}"`
362
787
  );
363
788
  }
364
- return type;
365
789
  }
366
- var SQL_RESERVED_KEYWORDS, InvalidIdentifierError, SAFE_TYPE_PATTERN;
790
+ function forbiddenCheckTokenAt(sql, index) {
791
+ const ch = sql[index];
792
+ if (ch === ";" || ch === "\\") return ch;
793
+ const pair = sql.slice(index, index + 2);
794
+ if (pair === "--" || pair === "/*" || pair === "*/") return pair;
795
+ return void 0;
796
+ }
797
+ function readDollarQuoteDelimiter(sql, index) {
798
+ if (sql[index] !== "$") return void 0;
799
+ const previous = index > 0 ? sql[index - 1] : void 0;
800
+ if (previous && isIdentifierContinuationBeforeDollarQuote(previous)) {
801
+ return void 0;
802
+ }
803
+ if (sql[index + 1] === "$") return "$$";
804
+ if (!isDollarQuoteTagStart(sql[index + 1])) return void 0;
805
+ let cursor = index + 2;
806
+ while (isDollarQuoteTagContinuation(sql[cursor])) {
807
+ cursor++;
808
+ }
809
+ if (sql[cursor] !== "$") return void 0;
810
+ return sql.slice(index, cursor + 1);
811
+ }
812
+ function isIdentifierContinuationBeforeDollarQuote(ch) {
813
+ return /[A-Za-z0-9_$]/.test(ch) || (ch.codePointAt(0) ?? 0) > 127;
814
+ }
815
+ function isDollarQuoteTagStart(ch) {
816
+ return ch !== void 0 && /[A-Za-z_]/.test(ch);
817
+ }
818
+ function isDollarQuoteTagContinuation(ch) {
819
+ return ch !== void 0 && /[A-Za-z0-9_]/.test(ch);
820
+ }
821
+ function validateDbTypeName(type) {
822
+ assertString(type, "Unsafe database type name");
823
+ return validateDbType(type);
824
+ }
825
+ var SQL_RESERVED_KEYWORDS, InvalidIdentifierError, SAFE_DECIMAL_LITERAL, SAFE_STORAGE_BOOLEAN_LITERAL, SAFE_SIMPLE_IDENTIFIER_VALUE;
367
826
  var init_validate = __esm({
368
827
  "src/validate.ts"() {
369
828
  "use strict";
829
+ init_db_type();
370
830
  SQL_RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
371
831
  "all",
372
832
  "analyse",
@@ -482,7 +942,9 @@ var init_validate = __esm({
482
942
  identifierType;
483
943
  reason;
484
944
  };
485
- SAFE_TYPE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_ ]*(\(\d+(,\s*\d+)?\))?(\[\])?$/;
945
+ SAFE_DECIMAL_LITERAL = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/;
946
+ SAFE_STORAGE_BOOLEAN_LITERAL = /^(?:true|false|on|off)$/i;
947
+ SAFE_SIMPLE_IDENTIFIER_VALUE = /^[A-Za-z_][A-Za-z0-9_]*$/;
486
948
  }
487
949
  });
488
950
 
@@ -1126,7 +1588,7 @@ var init_param_value = __esm({
1126
1588
  });
1127
1589
 
1128
1590
  // src/handlers/expression/case-value.ts
1129
- function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler) {
1591
+ function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler, expressionHandler) {
1130
1592
  if (value === null || value === void 0) {
1131
1593
  return nullConstNode();
1132
1594
  }
@@ -1160,7 +1622,8 @@ function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler
1160
1622
  schema,
1161
1623
  naming,
1162
1624
  state,
1163
- nestedCaseHandler
1625
+ nestedCaseHandler,
1626
+ expressionHandler
1164
1627
  );
1165
1628
  const right = resolveCaseValue(
1166
1629
  expr.right,
@@ -1168,7 +1631,8 @@ function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler
1168
1631
  schema,
1169
1632
  naming,
1170
1633
  state,
1171
- nestedCaseHandler
1634
+ nestedCaseHandler,
1635
+ expressionHandler
1172
1636
  );
1173
1637
  return {
1174
1638
  A_Expr: {
@@ -1186,15 +1650,29 @@ function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler
1186
1650
  }
1187
1651
  // falls through
1188
1652
  default: {
1653
+ if (expressionHandler && typeof expr.kind === "string" && EXPRESSION_HANDLER_KINDS.has(expr.kind)) {
1654
+ return expressionHandler(expr);
1655
+ }
1189
1656
  return bindParameter(value, state);
1190
1657
  }
1191
1658
  }
1192
1659
  }
1660
+ var EXPRESSION_HANDLER_KINDS;
1193
1661
  var init_case_value = __esm({
1194
1662
  "src/handlers/expression/case-value.ts"() {
1195
1663
  "use strict";
1196
1664
  init_ast_helpers();
1197
1665
  init_param_value();
1666
+ EXPRESSION_HANDLER_KINDS = /* @__PURE__ */ new Set([
1667
+ "customOp",
1668
+ "customFn",
1669
+ "ref",
1670
+ "cast",
1671
+ "unary",
1672
+ "array",
1673
+ "subquery",
1674
+ "relationColumn"
1675
+ ]);
1198
1676
  }
1199
1677
  });
1200
1678
 
@@ -2752,7 +3230,7 @@ var init_raw_expression_parser = __esm({
2752
3230
  });
2753
3231
 
2754
3232
  // src/compiler-utils.ts
2755
- import { InvalidOperationError, validateTypeName as validateTypeName2 } from "@dbsp/core";
3233
+ import { InvalidOperationError } from "@dbsp/core";
2756
3234
  function inferPgArrayType(columnName, columnTypes, sampleValue) {
2757
3235
  if (columnTypes?.[columnName]) {
2758
3236
  const pgBase = mapToPgBaseType(columnTypes[columnName]);
@@ -2770,7 +3248,16 @@ function inferPgArrayType(columnName, columnTypes, sampleValue) {
2770
3248
  return "text[]";
2771
3249
  }
2772
3250
  function mapToPgBaseType(pgType) {
2773
- const normalized = pgType.toUpperCase().replace(/\(.*\)/, "").trim();
3251
+ const trimmedPgType = pgType.trim();
3252
+ try {
3253
+ validateDbType(trimmedPgType);
3254
+ } catch {
3255
+ throw new Error(
3256
+ `batchValues: invalid type name '${pgType}'. Must be a structurally valid PostgreSQL type name.`
3257
+ );
3258
+ }
3259
+ const elementType = stripArraySuffix(trimmedPgType);
3260
+ const normalized = elementType.toUpperCase().replace(/\(.*\)/, "").trim();
2774
3261
  switch (normalized) {
2775
3262
  // ColumnType aliases (lowercase ColumnType values from ModelIR)
2776
3263
  case "STRING":
@@ -2800,9 +3287,10 @@ function mapToPgBaseType(pgType) {
2800
3287
  case "DOUBLE PRECISION":
2801
3288
  case "FLOAT8":
2802
3289
  case "FLOAT":
3290
+ return "float8";
2803
3291
  case "NUMERIC":
2804
3292
  case "DECIMAL":
2805
- return "float8";
3293
+ return "numeric";
2806
3294
  case "TEXT":
2807
3295
  case "VARCHAR":
2808
3296
  case "CHAR":
@@ -2822,11 +3310,8 @@ function mapToPgBaseType(pgType) {
2822
3310
  return "timestamptz";
2823
3311
  case "DATE":
2824
3312
  return "date";
2825
- default: {
2826
- const customType = pgType.toLowerCase();
2827
- validateTypeName2(customType);
2828
- return customType;
2829
- }
3313
+ default:
3314
+ return elementType;
2830
3315
  }
2831
3316
  }
2832
3317
  function stripArraySuffix(pgArrayType) {
@@ -2875,6 +3360,7 @@ function parseRawExpression(sqlFragment) {
2875
3360
  var init_compiler_utils = __esm({
2876
3361
  "src/compiler-utils.ts"() {
2877
3362
  "use strict";
3363
+ init_db_type();
2878
3364
  init_raw_expression_parser();
2879
3365
  }
2880
3366
  });
@@ -2891,7 +3377,6 @@ var init_param_intent = __esm({
2891
3377
  });
2892
3378
 
2893
3379
  // src/handlers/where/utils.ts
2894
- import { validateTypeName as validateTypeName3 } from "@dbsp/core";
2895
3380
  import { isFieldRef, isParamIntent as isParamIntent3 } from "@dbsp/types";
2896
3381
  function buildColumnRef(column, ctx) {
2897
3382
  if (column.includes(".")) {
@@ -2955,8 +3440,8 @@ function resolveColumnPgType(columnName, ctx) {
2955
3440
  if (!column) return void 0;
2956
3441
  if (column.originalDbType) {
2957
3442
  const typeName = column.originalDbType.trim();
2958
- validateTypeName3(typeName);
2959
- return typeName;
3443
+ validateDbType(typeName);
3444
+ return dbTypeCastTarget(typeName);
2960
3445
  }
2961
3446
  return void 0;
2962
3447
  }
@@ -2964,6 +3449,7 @@ var init_utils = __esm({
2964
3449
  "src/handlers/where/utils.ts"() {
2965
3450
  "use strict";
2966
3451
  init_ast_helpers();
3452
+ init_db_type();
2967
3453
  init_param_intent();
2968
3454
  init_param_ref();
2969
3455
  init_types();
@@ -6659,7 +7145,21 @@ var init_arithmetic = __esm({
6659
7145
  // src/handlers/expression/case.ts
6660
7146
  function resolveCaseValue2(value, ctx, state) {
6661
7147
  const alias = ctx.currentAlias ?? ctx.rootTable;
6662
- return resolveCaseValue(value, alias, void 0, ctx.naming, state);
7148
+ return resolveCaseValue(
7149
+ value,
7150
+ alias,
7151
+ void 0,
7152
+ ctx.naming,
7153
+ state,
7154
+ void 0,
7155
+ // Handler-level path: renders every expression kind via the shared
7156
+ // expression compiler. It does NOT apply the customFn FILTER modifier
7157
+ // (agg_filter) — that requires compiler-private compileFilterCondition,
7158
+ // which cannot be reached here without a circular dependency. The primary
7159
+ // DX caseWhen path (PlanCompiler.compileCustomExpressionNode) filter-patches
7160
+ // correctly; use it for filtered aggregates as CASE branches.
7161
+ (expr) => compileExpressionIntent(expr, ctx, state)
7162
+ );
6663
7163
  }
6664
7164
  var caseHandler, simpleCaseHandler;
6665
7165
  var init_case = __esm({
@@ -6668,6 +7168,7 @@ var init_case = __esm({
6668
7168
  init_ast_helpers();
6669
7169
  init_param_intent();
6670
7170
  init_case_value();
7171
+ init_custom();
6671
7172
  init_param_value();
6672
7173
  caseHandler = {
6673
7174
  types: ["case", "CASE", "caseWhen"],
@@ -9366,6 +9867,24 @@ function renumberNode(value, offset) {
9366
9867
  }
9367
9868
  return result;
9368
9869
  }
9870
+ function maxParamRefNumber(value) {
9871
+ if (value === null || typeof value !== "object") return 0;
9872
+ if (Array.isArray(value)) {
9873
+ let max2 = 0;
9874
+ for (const item of value) max2 = Math.max(max2, maxParamRefNumber(item));
9875
+ return max2;
9876
+ }
9877
+ const obj = value;
9878
+ if ("ParamRef" in obj && obj.ParamRef !== null && typeof obj.ParamRef === "object") {
9879
+ const n = obj.ParamRef.number;
9880
+ return typeof n === "number" ? n : 0;
9881
+ }
9882
+ let max = 0;
9883
+ for (const key of Object.keys(obj)) {
9884
+ max = Math.max(max, maxParamRefNumber(obj[key]));
9885
+ }
9886
+ return max;
9887
+ }
9369
9888
  var includeHandlersInitialized = false;
9370
9889
  function ensureIncludeHandlersRegistered() {
9371
9890
  if (includeHandlersInitialized) return;
@@ -10400,7 +10919,7 @@ var PlanCompiler = class _PlanCompiler {
10400
10919
  }
10401
10920
  case "selectExpression": {
10402
10921
  if (decision.expressionType === "case") {
10403
- const caseNode = this.compileCaseExpression(decision);
10922
+ const caseNode = this.compileCaseExpression(decision, plan);
10404
10923
  const alias = decision.alias;
10405
10924
  targetList.push({
10406
10925
  ResTarget: {
@@ -10413,34 +10932,7 @@ var PlanCompiler = class _PlanCompiler {
10413
10932
  }
10414
10933
  case "selectCustomExpression": {
10415
10934
  const exprIntent = decision.expressionIntent;
10416
- const outerThis = this;
10417
- const ctx = {
10418
- ...this.createHandlerContext(plan, plan.rootTable),
10419
- compileSubquery(query, paramOffset) {
10420
- return outerThis.compileExpressionSubquery(query, paramOffset);
10421
- }
10422
- };
10423
- const state = this.createHandlerState();
10424
- const node = compileExpressionIntent(exprIntent, ctx, state);
10425
- if (exprIntent.kind === "customFn" && exprIntent.filter) {
10426
- const filterIntent = exprIntent.filter;
10427
- const filterDecision = convertWhereCondition(
10428
- filterIntent,
10429
- plan.rootTable
10430
- );
10431
- if (filterDecision) {
10432
- const filterNode = compileFilterCondition(
10433
- filterDecision,
10434
- createWhereDispatcher(),
10435
- ctx,
10436
- state
10437
- );
10438
- if (filterNode && "FuncCall" in node) {
10439
- node.FuncCall.agg_filter = filterNode;
10440
- }
10441
- }
10442
- }
10443
- this.state.paramIndex = state.paramIndex;
10935
+ const node = this.compileCustomExpressionNode(exprIntent, plan);
10444
10936
  const alias = decision.alias || decision.column || void 0;
10445
10937
  targetList.push({
10446
10938
  ResTarget: {
@@ -10707,14 +11199,28 @@ var PlanCompiler = class _PlanCompiler {
10707
11199
  */
10708
11200
  compileJoinDecision(decision, plan, from) {
10709
11201
  if (isPrecompiledJoinDecision(decision)) {
10710
- if (isBatchValuesJoinDecision(decision)) {
11202
+ const isBatchValues = isBatchValuesJoinDecision(decision);
11203
+ if (isBatchValues) {
10711
11204
  for (const p of decision.batchValuesParams) {
10712
11205
  this.state.parameters.push(p);
10713
11206
  }
10714
11207
  this.state.paramIndex = this.state.parameters.length;
10715
11208
  }
10716
11209
  const jRarg = decision.joinRarg;
10717
- const jOn = decision.joinOnNode;
11210
+ let jOn = decision.joinOnNode;
11211
+ if (!isBatchValues && decision.joinOnParams?.length) {
11212
+ if (maxParamRefNumber(decision.joinOnNode) > decision.joinOnParams.length) {
11213
+ throw new Error(
11214
+ "Table-mode JOIN ON clauses support only inline param() values; externally pre-assigned or bound parameters in an ON clause are not supported."
11215
+ );
11216
+ }
11217
+ const offset = this.state.parameters.length;
11218
+ jOn = renumberParamRefsInAst(decision.joinOnNode, offset);
11219
+ for (const p of decision.joinOnParams) {
11220
+ this.state.parameters.push(p);
11221
+ }
11222
+ this.state.paramIndex = this.state.parameters.length;
11223
+ }
10718
11224
  from[0] = decision.joinType === "left" ? leftJoin(from[0], jRarg, jOn) : innerJoin(from[0], jRarg, jOn);
10719
11225
  } else {
10720
11226
  from[0] = this.compileJoin(decision, plan, from[0]);
@@ -11008,7 +11514,7 @@ var PlanCompiler = class _PlanCompiler {
11008
11514
  // --------------------------------------------------------------------------
11009
11515
  // CASE Expression Compilation
11010
11516
  // --------------------------------------------------------------------------
11011
- compileCaseExpression(decision) {
11517
+ compileCaseExpression(decision, plan) {
11012
11518
  const conditions = decision.conditions;
11013
11519
  const elseValue = decision.value;
11014
11520
  if (!conditions || conditions.length === 0) {
@@ -11016,7 +11522,7 @@ var PlanCompiler = class _PlanCompiler {
11016
11522
  }
11017
11523
  const args = conditions.map((cond) => {
11018
11524
  const whenExpr = this.dispatchWhere(cond.when);
11019
- const thenResult = this.compileCaseValue(cond.then);
11525
+ const thenResult = this.compileCaseValue(cond.then, plan);
11020
11526
  return {
11021
11527
  CaseWhen: {
11022
11528
  expr: whenExpr,
@@ -11026,7 +11532,7 @@ var PlanCompiler = class _PlanCompiler {
11026
11532
  });
11027
11533
  let defresult;
11028
11534
  if (elseValue !== void 0) {
11029
- defresult = this.compileCaseValue(elseValue);
11535
+ defresult = this.compileCaseValue(elseValue, plan);
11030
11536
  }
11031
11537
  return {
11032
11538
  CaseExpr: {
@@ -11039,26 +11545,66 @@ var PlanCompiler = class _PlanCompiler {
11039
11545
  * Compile a CASE THEN/ELSE value based on its ExpressionIntent kind.
11040
11546
  * Delegates to shared resolveCaseValue with nested CASE support.
11041
11547
  */
11042
- compileCaseValue(value) {
11548
+ compileCaseValue(value, plan) {
11043
11549
  return resolveCaseValue(
11044
11550
  value,
11045
11551
  this.currentRootTable,
11046
11552
  void 0,
11047
11553
  this.naming,
11048
11554
  this.state,
11049
- (expr) => this.compileCaseExpression({
11050
- type: "selectExpression",
11051
- expressionType: "case",
11052
- conditions: expr.when.map((wc) => ({
11053
- when: wc.condition,
11054
- // biome-ignore lint/suspicious/noThenProperty: intentional
11055
- then: wc.result
11056
- })),
11057
- value: expr.else,
11058
- table: this.currentRootTable
11059
- })
11555
+ (expr) => {
11556
+ const nestedDecision = {
11557
+ type: "selectExpression",
11558
+ expressionType: "case",
11559
+ conditions: expr.when.map((wc) => ({
11560
+ when: wc.condition,
11561
+ // biome-ignore lint/suspicious/noThenProperty: intentional
11562
+ then: wc.result
11563
+ })),
11564
+ value: expr.else,
11565
+ table: this.currentRootTable
11566
+ };
11567
+ return this.compileCaseExpression(nestedDecision, plan);
11568
+ },
11569
+ (expr) => this.compileCustomExpressionNode(
11570
+ expr,
11571
+ plan
11572
+ )
11060
11573
  );
11061
11574
  }
11575
+ /**
11576
+ * Compile a custom ExpressionIntent (customFn, customOp, ref, cast, unary,
11577
+ * array, function, subquery, …) to an AST node, applying the customFn FILTER
11578
+ * clause. Shared by the `selectCustomExpression` target path and CASE
11579
+ * THEN/ELSE values so both render the full expression surface identically
11580
+ * (every expression kind + FILTER), rather than one path silently binding
11581
+ * expressions as parameters or dropping FILTER.
11582
+ */
11583
+ compileCustomExpressionNode(exprIntent, plan) {
11584
+ const ctx = this.createHandlerContext(plan, plan.rootTable);
11585
+ const state = this.createHandlerState();
11586
+ const node = compileExpressionIntent(exprIntent, ctx, state);
11587
+ if (exprIntent.kind === "customFn" && exprIntent.filter) {
11588
+ const filterIntent = exprIntent.filter;
11589
+ const filterDecision = convertWhereCondition(
11590
+ filterIntent,
11591
+ plan.rootTable
11592
+ );
11593
+ if (filterDecision) {
11594
+ const filterNode = compileFilterCondition(
11595
+ filterDecision,
11596
+ createWhereDispatcher(),
11597
+ ctx,
11598
+ state
11599
+ );
11600
+ if (filterNode && "FuncCall" in node) {
11601
+ node.FuncCall.agg_filter = filterNode;
11602
+ }
11603
+ }
11604
+ }
11605
+ this.state.paramIndex = state.paramIndex;
11606
+ return node;
11607
+ }
11062
11608
  // --------------------------------------------------------------------------
11063
11609
  // INSERT Compilation
11064
11610
  // --------------------------------------------------------------------------
@@ -11261,6 +11807,9 @@ function compilePlan(plan, options) {
11261
11807
  init_naming_plugin();
11262
11808
  init_validate();
11263
11809
 
11810
+ // src/ddl/phases/comments.ts
11811
+ init_validate();
11812
+
11264
11813
  // src/ddl/phases/types.ts
11265
11814
  function sup(caps, flag) {
11266
11815
  return !caps || flag === true;
@@ -11281,6 +11830,7 @@ var VALID_INDEX_METHODS = /* @__PURE__ */ new Set([
11281
11830
  "bloom"
11282
11831
  ]);
11283
11832
  function validateIndexMethod(method, context = "index method") {
11833
+ assertString(method, `Invalid ${context}`);
11284
11834
  if (!VALID_INDEX_METHODS.has(method)) {
11285
11835
  throw new Error(
11286
11836
  `Invalid ${context}: "${method}". Must be one of: ${[...VALID_INDEX_METHODS].join(", ")}`
@@ -11288,6 +11838,7 @@ function validateIndexMethod(method, context = "index method") {
11288
11838
  }
11289
11839
  }
11290
11840
  function validateEnumLabel(value, context = "enum label") {
11841
+ assertString(value, `Invalid ${context}`);
11291
11842
  if (/\x00/.test(value)) {
11292
11843
  throw new Error(
11293
11844
  `Invalid ${context}: contains NUL byte (\\x00) which would be silently truncated by PostgreSQL`
@@ -11312,6 +11863,7 @@ function quoteCollation(name) {
11312
11863
  return `"${name}"`;
11313
11864
  }
11314
11865
  function quoteRoleName(name) {
11866
+ assertString(name, "quoteRoleName: role name");
11315
11867
  if (!name) {
11316
11868
  throw new Error("quoteRoleName: role name must not be empty");
11317
11869
  }
@@ -11378,17 +11930,21 @@ function generateCommentsPhase(ctx) {
11378
11930
  }
11379
11931
  const statements = [];
11380
11932
  for (const table of tables) {
11381
- if (table.comment) {
11933
+ const tableComment = table.comment;
11934
+ if (tableComment) {
11935
+ assertString(tableComment, "table comment");
11382
11936
  const qualifiedTable = qualifyTableIdent(table.name, schemaName, naming);
11383
11937
  statements.push(
11384
- `COMMENT ON TABLE ${qualifiedTable} IS '${table.comment.replace(/'/g, "''")}';`
11938
+ `COMMENT ON TABLE ${qualifiedTable} IS '${tableComment.replace(/'/g, "''")}';`
11385
11939
  );
11386
11940
  }
11387
11941
  for (const col of table.columns) {
11388
- if (col.comment) {
11942
+ const columnComment = col.comment;
11943
+ if (columnComment) {
11944
+ assertString(columnComment, "column comment");
11389
11945
  const qualifiedTable = qualifyTableIdent(table.name, schemaName, naming);
11390
11946
  statements.push(
11391
- `COMMENT ON COLUMN ${qualifiedTable}.${quoteIdent2(naming.toDatabase(col.name))} IS '${col.comment.replace(/'/g, "''")}';`
11947
+ `COMMENT ON COLUMN ${qualifiedTable}.${quoteIdent2(naming.toDatabase(col.name))} IS '${columnComment.replace(/'/g, "''")}';`
11392
11948
  );
11393
11949
  }
11394
11950
  }
@@ -11413,9 +11969,10 @@ function generateConstraintsPhase(ctx) {
11413
11969
  for (const check of table.checkConstraints ?? []) {
11414
11970
  const qualifiedTable = qualifyTableIdent(table.name, schemaName, naming);
11415
11971
  const constraintName = quoteIdent2(check.name);
11416
- validateSqlExpression(check.expression, "check constraint expression");
11972
+ const expression = check.expression;
11973
+ validateCheckExpression(expression, "check constraint expression");
11417
11974
  statements.push(
11418
- `ALTER TABLE ${qualifiedTable} ADD CONSTRAINT ${constraintName} ${check.expression};`
11975
+ `ALTER TABLE ${qualifiedTable} ADD CONSTRAINT ${constraintName} ${expression};`
11419
11976
  );
11420
11977
  }
11421
11978
  }
@@ -11525,16 +12082,37 @@ function generateRlsPhase(ctx) {
11525
12082
  init_validate();
11526
12083
 
11527
12084
  // src/ddl/type-mapping.ts
12085
+ init_db_type();
11528
12086
  init_validate();
11529
12087
  function mapColumnType(col) {
11530
12088
  if (col.originalDbType) {
11531
- return validateDbTypeName(col.originalDbType.toUpperCase()).toUpperCase();
12089
+ const originalDbType = validateDbTypeName(col.originalDbType);
12090
+ return isPgBuiltInTypeName(originalDbType) ? uppercaseOutsideQuotedIdentifiers(originalDbType) : originalDbType;
11532
12091
  }
11533
12092
  if (col.autoIncrement) {
11534
12093
  return col.type === "bigint" ? "BIGSERIAL" : "SERIAL";
11535
12094
  }
11536
12095
  return mapBaseType(col.type);
11537
12096
  }
12097
+ function uppercaseOutsideQuotedIdentifiers(type) {
12098
+ let result = "";
12099
+ let inQuotedIdentifier = false;
12100
+ for (let i = 0; i < type.length; i++) {
12101
+ const char = type[i];
12102
+ if (char === '"') {
12103
+ result += char;
12104
+ if (inQuotedIdentifier && type[i + 1] === '"') {
12105
+ result += type[i + 1];
12106
+ i++;
12107
+ continue;
12108
+ }
12109
+ inQuotedIdentifier = !inQuotedIdentifier;
12110
+ continue;
12111
+ }
12112
+ result += inQuotedIdentifier ? char : char.toUpperCase();
12113
+ }
12114
+ return result;
12115
+ }
11538
12116
  function mapBaseType(type) {
11539
12117
  switch (type) {
11540
12118
  case "string":
@@ -11598,12 +12176,29 @@ function mapOnDeleteAction(action) {
11598
12176
  function qualifyTable(table, schemaName) {
11599
12177
  return schemaName ? `${quoteIdent2(schemaName, "schema")}.${quoteIdent2(table, "table")}` : quoteIdent2(table, "table");
11600
12178
  }
12179
+ function buildDoBlock(body) {
12180
+ const delimiter = chooseDoBlockDelimiter(body);
12181
+ return `DO ${delimiter} ${body} ${delimiter};`;
12182
+ }
12183
+ function chooseDoBlockDelimiter(body) {
12184
+ if (!body.includes("$$")) return "$$";
12185
+ let suffix = 0;
12186
+ let delimiter = "$dbsp_check$";
12187
+ while (body.includes(delimiter)) {
12188
+ suffix++;
12189
+ delimiter = `$dbsp_check_${suffix}$`;
12190
+ }
12191
+ return delimiter;
12192
+ }
11601
12193
  function pkName(table) {
11602
12194
  return `pk_${table}`;
11603
12195
  }
11604
12196
  function fkName(table, columns) {
11605
12197
  return `fk_${table}_${columns.join("_")}`;
11606
12198
  }
12199
+ function uniqueName(table, column) {
12200
+ return `${table}_${column}_key`;
12201
+ }
11607
12202
  function idxName(table, columns, customName) {
11608
12203
  return customName ?? `idx_${table}_${columns.join("_")}`;
11609
12204
  }
@@ -11658,11 +12253,22 @@ function buildPolicySQL(tableName, policy, schemaName) {
11658
12253
  }
11659
12254
  function buildSequenceClause(verb, seqName, seq, includeCycleNoCycle = false) {
11660
12255
  const parts = [`${verb} ${seqName}`];
11661
- if (seq.startWith !== void 0) parts.push(`START WITH ${seq.startWith}`);
12256
+ if (seq.startWith !== void 0)
12257
+ parts.push(
12258
+ `START WITH ${assertNumericLiteral(seq.startWith, "sequence START WITH")}`
12259
+ );
11662
12260
  if (seq.incrementBy !== void 0)
11663
- parts.push(`INCREMENT BY ${seq.incrementBy}`);
11664
- if (seq.minValue !== void 0) parts.push(`MINVALUE ${seq.minValue}`);
11665
- if (seq.maxValue !== void 0) parts.push(`MAXVALUE ${seq.maxValue}`);
12261
+ parts.push(
12262
+ `INCREMENT BY ${assertNumericLiteral(seq.incrementBy, "sequence INCREMENT BY")}`
12263
+ );
12264
+ if (seq.minValue !== void 0)
12265
+ parts.push(
12266
+ `MINVALUE ${assertNumericLiteral(seq.minValue, "sequence MINVALUE")}`
12267
+ );
12268
+ if (seq.maxValue !== void 0)
12269
+ parts.push(
12270
+ `MAXVALUE ${assertNumericLiteral(seq.maxValue, "sequence MAXVALUE")}`
12271
+ );
11666
12272
  if (includeCycleNoCycle) {
11667
12273
  if (seq.cycle !== void 0) parts.push(seq.cycle ? "CYCLE" : "NO CYCLE");
11668
12274
  } else {
@@ -11672,7 +12278,7 @@ function buildSequenceClause(verb, seqName, seq, includeCycleNoCycle = false) {
11672
12278
  }
11673
12279
  function failSafeUnknownDownChange(kind) {
11674
12280
  return {
11675
- sql: `-- WARNING: Cannot reverse unsupported SchemaChange kind "${kind}"`,
12281
+ sql: `-- WARNING: Cannot reverse unsupported SchemaChange kind "${sanitizeCommentText(kind)}"`,
11676
12282
  destructive: true
11677
12283
  };
11678
12284
  }
@@ -11734,7 +12340,7 @@ function generateMigrationSQL(diff, options) {
11734
12340
  [],
11735
12341
  // 8: alter column
11736
12342
  [],
11737
- // 9: add PK
12343
+ // 9: add PK / column UNIQUE constraint
11738
12344
  [],
11739
12345
  // 10: add FK
11740
12346
  [],
@@ -11821,6 +12427,7 @@ function getPhase(kind) {
11821
12427
  case "alter_column_collation":
11822
12428
  case "alter_column_identity":
11823
12429
  return 8;
12430
+ case "alter_column_unique":
11824
12431
  case "add_primary_key":
11825
12432
  return 9;
11826
12433
  case "add_foreign_key":
@@ -11857,7 +12464,7 @@ function upAddColumn(change, schemaName) {
11857
12464
  }
11858
12465
  function upAlterColumnType(change, schemaName) {
11859
12466
  const col = change.meta?.column;
11860
- const toType = col ? mapColumnType(col) : String(change.meta?.toType);
12467
+ const toType = col ? mapColumnType(col) : validateDbTypeName(change.meta?.toType);
11861
12468
  return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} TYPE ${toType};`;
11862
12469
  }
11863
12470
  function upAlterColumnNullable(change, schemaName) {
@@ -11872,6 +12479,34 @@ function upAlterColumnDefault(change, schemaName) {
11872
12479
  }
11873
12480
  return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} SET DEFAULT ${formatDefault(def)};`;
11874
12481
  }
12482
+ function addColumnUniqueSQL(table, column, schemaName, constraintName) {
12483
+ const name = constraintName ?? uniqueName(table, column);
12484
+ if (constraintName !== void 0) validateIdentifier(constraintName, "alias");
12485
+ const quotedConstraintName = quoteIdent2(name, "alias");
12486
+ return `ALTER TABLE ${qualifyTable(table, schemaName)} ADD CONSTRAINT ${quotedConstraintName} UNIQUE (${quoteIdent2(column, "alias")});`;
12487
+ }
12488
+ function dropColumnUniqueSQL(table, column, schemaName, constraintName) {
12489
+ const name = constraintName ?? uniqueName(table, column);
12490
+ if (constraintName !== void 0) validateIdentifier(constraintName, "alias");
12491
+ const quotedConstraintName = quoteIdent2(name, "alias");
12492
+ return `ALTER TABLE ${qualifyTable(table, schemaName)} DROP CONSTRAINT IF EXISTS ${quotedConstraintName};`;
12493
+ }
12494
+ function upAlterColumnUnique(change, schemaName) {
12495
+ const unique = change.meta?.unique;
12496
+ if (unique === void 0 || !change.column) return void 0;
12497
+ const constraintName = change.meta?.constraintName;
12498
+ return unique ? addColumnUniqueSQL(
12499
+ change.table,
12500
+ change.column,
12501
+ schemaName,
12502
+ constraintName
12503
+ ) : dropColumnUniqueSQL(
12504
+ change.table,
12505
+ change.column,
12506
+ schemaName,
12507
+ constraintName
12508
+ );
12509
+ }
11875
12510
  function upAddPrimaryKey(change, schemaName) {
11876
12511
  const columns = change.meta?.columns;
11877
12512
  const pkCols = columns.map((n) => quoteIdent2(n, "alias")).join(", ");
@@ -11900,8 +12535,9 @@ function upCreateIndex(change, schemaName) {
11900
12535
  "alias"
11901
12536
  );
11902
12537
  const unique = idx.unique ? "UNIQUE " : "";
11903
- if (idx.method) validateIndexMethod(idx.method, "index method");
11904
- const method = idx.method ? ` USING ${idx.method}` : "";
12538
+ const indexMethod = idx.method;
12539
+ if (indexMethod) validateIndexMethod(indexMethod, "index method");
12540
+ const method = indexMethod ? ` USING ${indexMethod}` : "";
11905
12541
  const colParts = [
11906
12542
  ...(idx.expressions ?? []).map((expr) => {
11907
12543
  validateSqlExpression(expr, "index expression");
@@ -11918,10 +12554,11 @@ function upCreateIndex(change, schemaName) {
11918
12554
  const nullsNotDistinct = idx.unique && idx.nullsNotDistinct ? " NULLS NOT DISTINCT" : "";
11919
12555
  const withParams = idx.with && Object.keys(idx.with).length > 0 ? ` WITH (${Object.entries(idx.with).map(([k, v]) => {
11920
12556
  validateIdentifier(k, "alias");
11921
- return `${k} = ${v}`;
12557
+ return `${k} = ${formatStorageParameterValue(v, `index WITH parameter "${k}"`)}`;
11922
12558
  }).join(", ")})` : "";
11923
- if (idx.where) validateSqlExpression(idx.where, "index WHERE predicate");
11924
- const where = idx.where ? ` WHERE ${idx.where}` : "";
12559
+ const whereExpr = idx.where;
12560
+ if (whereExpr) validateSqlExpression(whereExpr, "index WHERE predicate");
12561
+ const where = whereExpr ? ` WHERE ${whereExpr}` : "";
11925
12562
  return `CREATE ${unique}INDEX IF NOT EXISTS ${indexName} ON ${qualifyTable(change.table, schemaName)}${method} (${cols})${include}${nullsNotDistinct}${withParams}${where};`;
11926
12563
  }
11927
12564
  function upDropIndex(change, schemaName) {
@@ -11937,12 +12574,12 @@ function upDropIndex(change, schemaName) {
11937
12574
  function upAddCheckConstraint(change, schemaName) {
11938
12575
  const check = change.meta?.check;
11939
12576
  if (!check) return void 0;
12577
+ const expression = check.expression;
11940
12578
  const notValid = check.notValid ? " NOT VALID" : "";
11941
- validateSqlExpression(
11942
- check.expression,
11943
- "migration check constraint expression"
12579
+ validateCheckExpression(expression, "migration check constraint expression");
12580
+ return buildDoBlock(
12581
+ "BEGIN ALTER TABLE " + qualifyTable(change.table, schemaName) + " ADD CONSTRAINT " + quoteIdent2(check.name, "alias") + " " + expression + notValid + "; EXCEPTION WHEN duplicate_object THEN NULL; END"
11944
12582
  );
11945
- return "DO $$ BEGIN ALTER TABLE " + qualifyTable(change.table, schemaName) + " ADD CONSTRAINT " + quoteIdent2(check.name, "alias") + " " + check.expression + notValid + "; EXCEPTION WHEN duplicate_object THEN NULL; END $$;";
11946
12583
  }
11947
12584
  function upValidateConstraint(change, schemaName) {
11948
12585
  const fk = change.meta?.fk;
@@ -11978,9 +12615,12 @@ function upDropEnum(change, schemaName) {
11978
12615
  if (!enumDef) return void 0;
11979
12616
  const enumName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(enumDef.name, "alias")}` : quoteIdent2(enumDef.name, "alias");
11980
12617
  const refs = change.meta?.referencingColumns;
11981
- const alterStatements = refs && refs.length > 0 ? refs.map(
11982
- (ref) => `ALTER TABLE ${qualifyTable(ref.table, schemaName)} ALTER COLUMN ${quoteIdent2(ref.column, "alias")} TYPE text;`
11983
- ) : [];
12618
+ const alterStatements = refs && refs.length > 0 ? refs.map((ref) => {
12619
+ const column = quoteIdent2(ref.column, "alias");
12620
+ const targetType = ref.isArray ? "text[]" : "text";
12621
+ const using = ref.isArray ? ` USING ${column}::text[]` : "";
12622
+ return `ALTER TABLE ${qualifyTable(ref.table, schemaName)} ALTER COLUMN ${column} TYPE ${targetType}${using};`;
12623
+ }) : [];
11984
12624
  return [...alterStatements, `DROP TYPE IF EXISTS ${enumName} CASCADE;`].join(
11985
12625
  "\n"
11986
12626
  );
@@ -12011,6 +12651,7 @@ function upAlterColumnIdentity(change, schemaName) {
12011
12651
  function upAddComment(change, schemaName) {
12012
12652
  const comment = change.meta?.comment;
12013
12653
  const target = change.meta?.target;
12654
+ assertString(comment, "migration comment");
12014
12655
  const escaped = comment.replace(/'/g, "''");
12015
12656
  if (target === "table") {
12016
12657
  return `COMMENT ON TABLE ${qualifyTable(change.table, schemaName)} IS '${escaped}';`;
@@ -12038,6 +12679,8 @@ function changeToUpSQL(change, schemaName) {
12038
12679
  return upAlterColumnNullable(change, schemaName);
12039
12680
  case "alter_column_default":
12040
12681
  return upAlterColumnDefault(change, schemaName);
12682
+ case "alter_column_unique":
12683
+ return upAlterColumnUnique(change, schemaName);
12041
12684
  case "add_primary_key":
12042
12685
  return upAddPrimaryKey(change, schemaName);
12043
12686
  case "drop_primary_key":
@@ -12135,7 +12778,7 @@ function changeToDownSQL(change, schemaName) {
12135
12778
  };
12136
12779
  case "drop_table":
12137
12780
  return {
12138
- sql: `-- WARNING: Cannot reverse drop_table "${change.table}" -- table data was lost`,
12781
+ sql: `-- WARNING: Cannot reverse drop_table "${sanitizeCommentText(change.table)}" -- table data was lost`,
12139
12782
  destructive: true
12140
12783
  };
12141
12784
  case "add_column":
@@ -12145,19 +12788,20 @@ function changeToDownSQL(change, schemaName) {
12145
12788
  };
12146
12789
  case "drop_column":
12147
12790
  return {
12148
- sql: `-- WARNING: Cannot reverse drop_column "${change.table}"."${change.column}" -- column data was lost`,
12791
+ sql: `-- WARNING: Cannot reverse drop_column "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- column data was lost`,
12149
12792
  destructive: true
12150
12793
  };
12151
12794
  case "alter_column_type": {
12152
12795
  const fromType = change.meta?.fromType;
12153
- if (!fromType) {
12796
+ if (fromType == null) {
12154
12797
  return {
12155
- sql: `-- WARNING: Cannot reverse alter_column_type "${change.table}"."${change.column}" -- missing migration metadata`,
12798
+ sql: `-- WARNING: Cannot reverse alter_column_type "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- missing migration metadata`,
12156
12799
  destructive: true
12157
12800
  };
12158
12801
  }
12802
+ const safeFromType = validateDbTypeName(fromType);
12159
12803
  return {
12160
- sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} TYPE ${fromType};`,
12804
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} TYPE ${safeFromType};`,
12161
12805
  destructive: true
12162
12806
  };
12163
12807
  }
@@ -12165,7 +12809,7 @@ function changeToDownSQL(change, schemaName) {
12165
12809
  const oldNullable = change.meta?.oldNullable;
12166
12810
  if (oldNullable === void 0) {
12167
12811
  return {
12168
- sql: `-- WARNING: Cannot reverse alter_column_nullable "${change.table}"."${change.column}" -- missing migration metadata`,
12812
+ sql: `-- WARNING: Cannot reverse alter_column_nullable "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- missing migration metadata`,
12169
12813
  destructive: true
12170
12814
  };
12171
12815
  }
@@ -12180,7 +12824,7 @@ function changeToDownSQL(change, schemaName) {
12180
12824
  const oldDefault = change.meta?.oldDefault;
12181
12825
  if (oldDefault === void 0) {
12182
12826
  return {
12183
- sql: `-- WARNING: Cannot reverse alter_column_default "${change.table}"."${change.column}" -- missing migration metadata`,
12827
+ sql: `-- WARNING: Cannot reverse alter_column_default "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- missing migration metadata`,
12184
12828
  destructive: true
12185
12829
  };
12186
12830
  }
@@ -12197,6 +12841,30 @@ function changeToDownSQL(change, schemaName) {
12197
12841
  destructive: false
12198
12842
  };
12199
12843
  }
12844
+ case "alter_column_unique": {
12845
+ const unique = change.meta?.unique;
12846
+ if (unique === void 0 || !change.column) {
12847
+ return {
12848
+ sql: `-- WARNING: Cannot reverse alter_column_unique "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- missing migration metadata`,
12849
+ destructive: true
12850
+ };
12851
+ }
12852
+ const constraintName = change.meta?.constraintName;
12853
+ return {
12854
+ sql: unique ? dropColumnUniqueSQL(
12855
+ change.table,
12856
+ change.column,
12857
+ schemaName,
12858
+ constraintName
12859
+ ) : addColumnUniqueSQL(
12860
+ change.table,
12861
+ change.column,
12862
+ schemaName,
12863
+ constraintName
12864
+ ),
12865
+ destructive: unique === true
12866
+ };
12867
+ }
12200
12868
  case "add_primary_key": {
12201
12869
  return {
12202
12870
  sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${quoteIdent2(pkName(change.table), "alias")} CASCADE;`,
@@ -12214,7 +12882,7 @@ function changeToDownSQL(change, schemaName) {
12214
12882
  };
12215
12883
  }
12216
12884
  return {
12217
- sql: `-- WARNING: Cannot reverse drop_primary_key "${change.table}" -- columns unknown`,
12885
+ sql: `-- WARNING: Cannot reverse drop_primary_key "${sanitizeCommentText(change.table)}" -- columns unknown`,
12218
12886
  destructive: true
12219
12887
  };
12220
12888
  }
@@ -12234,7 +12902,7 @@ function changeToDownSQL(change, schemaName) {
12234
12902
  const fk = change.meta?.fk;
12235
12903
  if (!fk) {
12236
12904
  return {
12237
- sql: `-- WARNING: Cannot reverse drop_foreign_key "${change.table}" -- FK definition was lost`,
12905
+ sql: `-- WARNING: Cannot reverse drop_foreign_key "${sanitizeCommentText(change.table)}" -- FK definition was lost`,
12238
12906
  destructive: true
12239
12907
  };
12240
12908
  }
@@ -12249,7 +12917,7 @@ function changeToDownSQL(change, schemaName) {
12249
12917
  const fk = change.meta?.fk;
12250
12918
  if (!oldFk || !fk) {
12251
12919
  return {
12252
- sql: `-- WARNING: Cannot reverse alter_foreign_key "${change.table}" -- missing migration metadata`,
12920
+ sql: `-- WARNING: Cannot reverse alter_foreign_key "${sanitizeCommentText(change.table)}" -- missing migration metadata`,
12253
12921
  destructive: true
12254
12922
  };
12255
12923
  }
@@ -12279,7 +12947,7 @@ ${add}`, destructive: false };
12279
12947
  const idx = change.meta?.index;
12280
12948
  if (!idx) {
12281
12949
  return {
12282
- sql: `-- WARNING: Cannot reverse drop_index "${change.table}" -- index definition was lost`,
12950
+ sql: `-- WARNING: Cannot reverse drop_index "${sanitizeCommentText(change.table)}" -- index definition was lost`,
12283
12951
  destructive: true
12284
12952
  };
12285
12953
  }
@@ -12300,12 +12968,12 @@ ${add}`, destructive: false };
12300
12968
  case "drop_check_constraint": {
12301
12969
  const check = change.meta?.check;
12302
12970
  if (!check) return { sql: void 0, destructive: true };
12303
- validateSqlExpression(
12304
- check.expression,
12305
- "migration check constraint (down)"
12306
- );
12971
+ const expression = check.expression;
12972
+ validateCheckExpression(expression, "migration check constraint (down)");
12307
12973
  return {
12308
- sql: "DO $$ BEGIN ALTER TABLE " + qualifyTable(change.table, schemaName) + " ADD CONSTRAINT " + quoteIdent2(check.name, "alias") + " " + check.expression + "; EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
12974
+ sql: buildDoBlock(
12975
+ "BEGIN ALTER TABLE " + qualifyTable(change.table, schemaName) + " ADD CONSTRAINT " + quoteIdent2(check.name, "alias") + " " + expression + "; EXCEPTION WHEN duplicate_object THEN NULL; END"
12976
+ ),
12309
12977
  // Allowlisted: re-adds the dropped CHECK constraint from metadata.
12310
12978
  destructive: false
12311
12979
  };
@@ -12323,7 +12991,10 @@ ${add}`, destructive: false };
12323
12991
  const enumDef = change.meta?.enum;
12324
12992
  if (!enumDef) return { sql: void 0, destructive: true };
12325
12993
  const enumName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(enumDef.name, "alias")}` : quoteIdent2(enumDef.name, "alias");
12326
- const values = enumDef.values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
12994
+ const values = enumDef.values.map((v) => {
12995
+ validateEnumLabel(v, "enum value");
12996
+ return `'${v.replace(/'/g, "''")}'`;
12997
+ }).join(", ");
12327
12998
  return {
12328
12999
  sql: `CREATE TYPE ${enumName} AS ENUM (${values});`,
12329
13000
  // Allowlisted: re-creates the dropped enum type from metadata.
@@ -12337,7 +13008,7 @@ ${add}`, destructive: false };
12337
13008
  };
12338
13009
  case "alter_column_collation": {
12339
13010
  return {
12340
- sql: `-- WARNING: Cannot reverse alter_column_collation "${change.table}"."${change.column}" -- previous collation unknown`,
13011
+ sql: `-- WARNING: Cannot reverse alter_column_collation "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- previous collation unknown`,
12341
13012
  destructive: true
12342
13013
  };
12343
13014
  }
@@ -12363,7 +13034,7 @@ ${add}`, destructive: false };
12363
13034
  }
12364
13035
  if (!prevIdentity && !col.identity) {
12365
13036
  return {
12366
- sql: `-- WARNING: Cannot reverse alter_column_identity "${change.table}"."${change.column}" -- missing previous identity metadata`,
13037
+ sql: `-- WARNING: Cannot reverse alter_column_identity "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- missing previous identity metadata`,
12367
13038
  destructive: true
12368
13039
  };
12369
13040
  }
@@ -12390,7 +13061,8 @@ ${add}`, destructive: false };
12390
13061
  case "drop_comment": {
12391
13062
  const target = change.meta?.target;
12392
13063
  const comment = change.meta?.comment;
12393
- if (typeof comment === "string") {
13064
+ if (comment !== void 0) {
13065
+ assertString(comment, "migration comment");
12394
13066
  const escaped = comment.replace(/'/g, "''");
12395
13067
  if (target === "table") {
12396
13068
  return {
@@ -12407,8 +13079,9 @@ ${add}`, destructive: false };
12407
13079
  };
12408
13080
  }
12409
13081
  }
13082
+ const columnTarget2 = change.column === void 0 ? "" : `."${sanitizeCommentText(change.column)}"`;
12410
13083
  return {
12411
- sql: `-- WARNING: Cannot reverse drop_comment "${change.table}"${change.column ? `."${change.column}"` : ""} -- comment text was lost`,
13084
+ sql: '-- WARNING: Cannot reverse drop_comment "' + sanitizeCommentText(change.table) + '"' + columnTarget2 + " -- comment text was lost",
12412
13085
  destructive: true
12413
13086
  };
12414
13087
  }
@@ -12442,7 +13115,7 @@ ${add}`, destructive: false };
12442
13115
  const prevSeq = change.meta?.previousSequence;
12443
13116
  if (!prevSeq) {
12444
13117
  return {
12445
- sql: `-- WARNING: Cannot reverse alter_sequence "${change.table}" -- missing migration metadata`,
13118
+ sql: `-- WARNING: Cannot reverse alter_sequence "${sanitizeCommentText(change.table)}" -- missing migration metadata`,
12446
13119
  destructive: true
12447
13120
  };
12448
13121
  }
@@ -12465,7 +13138,7 @@ ${add}`, destructive: false };
12465
13138
  }
12466
13139
  case "validate_constraint":
12467
13140
  return {
12468
- sql: `-- VALIDATE CONSTRAINT cannot be reversed in PostgreSQL (table: "${change.table}")`,
13141
+ sql: `-- VALIDATE CONSTRAINT cannot be reversed in PostgreSQL (table: "${sanitizeCommentText(change.table)}")`,
12469
13142
  destructive: true
12470
13143
  };
12471
13144
  case "enable_rls":
@@ -12506,7 +13179,6 @@ function generateDownSQL(diff, options) {
12506
13179
  function generateDownMigrationSQL(diff, options) {
12507
13180
  const schemaName = options?.schemaName;
12508
13181
  const includeDestructive = options?.includeDestructive ?? true;
12509
- const changes = includeDestructive ? diff.changes : diff.changes.filter((c) => !c.destructive);
12510
13182
  const phases = [
12511
13183
  [],
12512
13184
  // 0: drop FK, drop CHECK
@@ -12527,7 +13199,7 @@ function generateDownMigrationSQL(diff, options) {
12527
13199
  [],
12528
13200
  // 8: alter column
12529
13201
  [],
12530
- // 9: add PK
13202
+ // 9: add PK / column UNIQUE constraint
12531
13203
  [],
12532
13204
  // 10: add FK
12533
13205
  [],
@@ -12547,7 +13219,7 @@ function generateDownMigrationSQL(diff, options) {
12547
13219
  []
12548
13220
  // 18: CREATE/DROP POLICY
12549
13221
  ];
12550
- for (const change of changes) {
13222
+ for (const change of diff.changes) {
12551
13223
  const phase = getPhase(change.kind);
12552
13224
  phases[phase].push(change);
12553
13225
  }
@@ -12556,6 +13228,7 @@ function generateDownMigrationSQL(diff, options) {
12556
13228
  for (let i = phases.length - 1; i >= 0; i--) {
12557
13229
  for (const change of phases[i]) {
12558
13230
  const down = changeToDownSQL(change, schemaName);
13231
+ if (!includeDestructive && down.destructive) continue;
12559
13232
  destructive = down.destructive || destructive;
12560
13233
  if (down.sql) statements.push(down.sql);
12561
13234
  }
@@ -12600,7 +13273,10 @@ function generateAddFKSQL(tableName, fk, schemaName) {
12600
13273
  const qualTable = qualifyTable(tableName, schemaName);
12601
13274
  const constraintName = quoteIdent2(fkName(tableName, fk.columns), "alias");
12602
13275
  const fkCols = fk.columns.map((n) => quoteIdent2(n, "alias")).join(", ");
12603
- const refTable = qualifyTable(fk.references.table, schemaName);
13276
+ const refTable = qualifyTable(
13277
+ fk.references.table,
13278
+ fk.references.schema ?? schemaName
13279
+ );
12604
13280
  const refCols = fk.references.columns.map((n) => quoteIdent2(n, "alias")).join(", ");
12605
13281
  const onDelete = fk.onDelete ? ` ON DELETE ${mapOnDeleteAction(fk.onDelete)}` : "";
12606
13282
  const onUpdate = fk.onUpdate ? ` ON UPDATE ${mapOnDeleteAction(fk.onUpdate)}` : "";
@@ -12686,10 +13362,11 @@ function quoteIdentifier(name) {
12686
13362
  validateIdentifier(name, "alias");
12687
13363
  return `"${name.replace(/"/g, '""')}"`;
12688
13364
  }
12689
- function qualifyTable2(tableName, schemaName, naming) {
13365
+ function qualifyTable2(tableName, schemaName, naming, transformSchema = true) {
12690
13366
  const table = quoteIdentifier(naming.toDatabase(tableName));
12691
13367
  if (schemaName) {
12692
- return `${quoteIdentifier(naming.toDatabase(schemaName))}.${table}`;
13368
+ const schema = transformSchema ? naming.toDatabase(schemaName) : schemaName;
13369
+ return `${quoteIdentifier(schema)}.${table}`;
12693
13370
  }
12694
13371
  return table;
12695
13372
  }
@@ -12760,7 +13437,7 @@ function generateAlterTableAddFK(tableName, fk, schemaName, naming) {
12760
13437
  `fk_${tableName}_${fk.columns.join("_")}`
12761
13438
  );
12762
13439
  const fkCols = fk.columns.map((col) => quoteIdentifier(naming.toDatabase(col))).join(", ");
12763
- const refTable = qualifyTable2(fk.references.table, schemaName, naming);
13440
+ const refTable = fk.references.schema !== void 0 ? qualifyTable2(fk.references.table, fk.references.schema, naming, false) : qualifyTable2(fk.references.table, schemaName, naming);
12764
13441
  const refCols = fk.references.columns.map((col) => quoteIdentifier(naming.toDatabase(col))).join(", ");
12765
13442
  const onDelete = fk.onDelete ? ` ON DELETE ${mapOnDeleteAction(fk.onDelete)}` : "";
12766
13443
  const onUpdate = fk.onUpdate ? ` ON UPDATE ${mapOnDeleteAction(fk.onUpdate)}` : "";
@@ -12774,8 +13451,9 @@ function generateCreateIndex(tableName, idx, schemaName, naming) {
12774
13451
  );
12775
13452
  const qualifiedTable = qualifyTable2(tableName, schemaName, naming);
12776
13453
  const unique = idx.unique ? "UNIQUE " : "";
12777
- if (idx.method) validateIndexMethod(idx.method, "index method");
12778
- const method = idx.method ? ` USING ${idx.method}` : "";
13454
+ const indexMethod = idx.method;
13455
+ if (indexMethod) validateIndexMethod(indexMethod, "index method");
13456
+ const method = indexMethod ? ` USING ${indexMethod}` : "";
12779
13457
  const colParts = [];
12780
13458
  if (idx.expressions && idx.expressions.length > 0) {
12781
13459
  for (const expr of idx.expressions) {
@@ -12795,10 +13473,11 @@ function generateCreateIndex(tableName, idx, schemaName, naming) {
12795
13473
  const nullsNotDistinct = idx.unique && idx.nullsNotDistinct ? " NULLS NOT DISTINCT" : "";
12796
13474
  const withParams = idx.with && Object.keys(idx.with).length > 0 ? ` WITH (${Object.entries(idx.with).map(([k, v]) => {
12797
13475
  validateIdentifier(k, "alias");
12798
- return `${k} = ${v}`;
13476
+ return `${k} = ${formatStorageParameterValue(v, `index WITH parameter "${k}"`)}`;
12799
13477
  }).join(", ")})` : "";
12800
- if (idx.where) validateSqlExpression(idx.where, "index WHERE predicate");
12801
- const where = idx.where ? ` WHERE ${idx.where}` : "";
13478
+ const whereExpr = idx.where;
13479
+ if (whereExpr) validateSqlExpression(whereExpr, "index WHERE predicate");
13480
+ const where = whereExpr ? ` WHERE ${whereExpr}` : "";
12802
13481
  return `CREATE ${unique}INDEX ${indexName} ON ${qualifiedTable}${method} (${cols})${include}${nullsNotDistinct}${withParams}${where};`;
12803
13482
  }
12804
13483
  function generateCreatePolicy(tableName, policy, schemaName, naming) {
@@ -13025,6 +13704,7 @@ async function removeMigrationRecord(pool, name) {
13025
13704
  }
13026
13705
 
13027
13706
  // src/ddl/schema-diff.ts
13707
+ init_db_type();
13028
13708
  init_naming_plugin();
13029
13709
  function compareSchemata(schema, db, options) {
13030
13710
  const changes = [];
@@ -13165,7 +13845,8 @@ function normalizeTable(table, plugin) {
13165
13845
  columns: fk.columns.map(toDb),
13166
13846
  references: {
13167
13847
  table: toDb(fk.references.table),
13168
- columns: fk.references.columns.map(toDb)
13848
+ columns: fk.references.columns.map(toDb),
13849
+ ...fk.references.schema !== void 0 ? { schema: fk.references.schema } : {}
13169
13850
  }
13170
13851
  })),
13171
13852
  indexes: table.indexes.map((idx) => ({
@@ -13211,9 +13892,7 @@ function compareColumns(schema, db, changes) {
13211
13892
  }
13212
13893
  }
13213
13894
  function compareColumnDetails(tableName, schema, db, changes) {
13214
- const schemaDbType = schema.originalDbType?.toLowerCase();
13215
- const dbDbType = db.originalDbType?.toLowerCase();
13216
- if (schemaDbType && dbDbType && schemaDbType !== dbDbType) {
13895
+ if (schema.originalDbType && db.originalDbType && !dbTypesEqual(schema.originalDbType, db.originalDbType)) {
13217
13896
  changes.push({
13218
13897
  kind: "alter_column_type",
13219
13898
  table: tableName,
@@ -13258,6 +13937,21 @@ function compareColumnDetails(tableName, schema, db, changes) {
13258
13937
  meta: { default: schema.default, oldDefault: db.default }
13259
13938
  });
13260
13939
  }
13940
+ const schemaUnique = schema.unique === true;
13941
+ const dbUnique = db.unique === true;
13942
+ if (schemaUnique !== dbUnique) {
13943
+ changes.push({
13944
+ kind: "alter_column_unique",
13945
+ table: tableName,
13946
+ column: schema.name,
13947
+ destructive: schemaUnique === false,
13948
+ details: `Change unique of "${schema.name}" from ${dbUnique} to ${schemaUnique}`,
13949
+ meta: {
13950
+ unique: schemaUnique,
13951
+ ...db.uniqueConstraintName !== void 0 ? { constraintName: db.uniqueConstraintName } : {}
13952
+ }
13953
+ });
13954
+ }
13261
13955
  if ((schema.collation ?? null) !== (db.collation ?? null)) {
13262
13956
  changes.push({
13263
13957
  kind: "alter_column_collation",
@@ -13329,7 +14023,8 @@ function comparePrimaryKeys(schema, db, changes) {
13329
14023
  kind: "drop_primary_key",
13330
14024
  table: schema.name,
13331
14025
  destructive: true,
13332
- details: `Drop primary key (${dbPK.join(", ")})`
14026
+ details: `Drop primary key (${dbPK.join(", ")})`,
14027
+ meta: { columns: dbPK }
13333
14028
  });
13334
14029
  return;
13335
14030
  }
@@ -13337,7 +14032,8 @@ function comparePrimaryKeys(schema, db, changes) {
13337
14032
  kind: "drop_primary_key",
13338
14033
  table: schema.name,
13339
14034
  destructive: true,
13340
- details: `Drop primary key (${dbPK.join(", ")})`
14035
+ details: `Drop primary key (${dbPK.join(", ")})`,
14036
+ meta: { columns: dbPK }
13341
14037
  });
13342
14038
  changes.push({
13343
14039
  kind: "add_primary_key",
@@ -13409,7 +14105,7 @@ function compareForeignKeys(schema, db, changes) {
13409
14105
  }
13410
14106
  }
13411
14107
  function fkKey(fk) {
13412
- return `${fk.columns.join(",")}\u2192${fk.references.table}(${fk.references.columns.join(",")})`;
14108
+ return `${fk.columns.join(",")}\u2192${fk.references.schema ?? ""}.${fk.references.table}(${fk.references.columns.join(",")})`;
13413
14109
  }
13414
14110
  function compareIndexes(schema, db, changes) {
13415
14111
  const explicitIndexCols = new Set(
@@ -13442,16 +14138,15 @@ function compareIndexes(schema, db, changes) {
13442
14138
  });
13443
14139
  }
13444
14140
  }
13445
- const autoUniqueIndexKeys = new Set(
13446
- schema.columns.filter((col) => col.unique === true).map(
13447
- (col) => indexKey({
13448
- columns: [col.name],
13449
- unique: true
13450
- })
13451
- )
14141
+ const dbColMap = new Map(db.columns.map((col) => [col.name, col]));
14142
+ const autoUniqueIndexColumns = new Set(
14143
+ schema.columns.filter((schemaCol) => {
14144
+ const dbCol = dbColMap.get(schemaCol.name);
14145
+ return dbCol !== void 0 && (schemaCol.unique === true || dbCol.unique === true);
14146
+ }).map((col) => col.name)
13452
14147
  );
13453
14148
  for (const [key, idx] of dbIdxMap) {
13454
- if (!schemaIdxMap.has(key) && !autoFkIndexKeys.has(key) && !autoUniqueIndexKeys.has(key)) {
14149
+ if (!schemaIdxMap.has(key) && !autoFkIndexKeys.has(key) && !isAutoUniqueIndex(schema.name, idx, autoUniqueIndexColumns)) {
13455
14150
  changes.push({
13456
14151
  kind: "drop_index",
13457
14152
  table: schema.name,
@@ -13462,6 +14157,16 @@ function compareIndexes(schema, db, changes) {
13462
14157
  }
13463
14158
  }
13464
14159
  }
14160
+ function isAutoUniqueIndex(tableName, idx, autoUniqueIndexColumns) {
14161
+ if (idx.columns.length !== 1) return false;
14162
+ const column = idx.columns[0];
14163
+ if (column === void 0 || !autoUniqueIndexColumns.has(column)) return false;
14164
+ if (idx.unique !== true) return false;
14165
+ if (idx.name !== void 0 && idx.name !== `${tableName}_${column}_key`) {
14166
+ return false;
14167
+ }
14168
+ return idx.nullsNotDistinct !== true && (idx.method === void 0 || idx.method === "btree") && idx.where === void 0 && (idx.expressions === void 0 || idx.expressions.length === 0) && (idx.include === void 0 || idx.include.length === 0) && (idx.opclass === void 0 || Object.keys(idx.opclass).length === 0) && (idx.with === void 0 || Object.keys(idx.with).length === 0);
14169
+ }
13465
14170
  function indexKey(idx) {
13466
14171
  const parts = [
13467
14172
  idx.columns.join(","),
@@ -13658,8 +14363,13 @@ function compareEnums(schema, db, changes) {
13658
14363
  const referencingColumns = [];
13659
14364
  for (const [tableName, tableIR] of db.tables) {
13660
14365
  for (const col of tableIR.columns) {
13661
- if (col.originalDbType === name) {
13662
- referencingColumns.push({ table: tableName, column: col.name });
14366
+ const kind = col.originalDbType !== void 0 ? enumReferenceKind(col.originalDbType, name) : null;
14367
+ if (kind !== null) {
14368
+ referencingColumns.push({
14369
+ table: tableName,
14370
+ column: col.name,
14371
+ ...kind === "array" ? { isArray: true } : {}
14372
+ });
13663
14373
  }
13664
14374
  }
13665
14375
  }
@@ -13826,6 +14536,7 @@ function buildSummary(changes) {
13826
14536
  case "alter_column_type":
13827
14537
  case "alter_column_nullable":
13828
14538
  case "alter_column_default":
14539
+ case "alter_column_unique":
13829
14540
  columns.altered++;
13830
14541
  break;
13831
14542
  case "add_primary_key":
@@ -14048,6 +14759,7 @@ function vectorDims(column) {
14048
14759
 
14049
14760
  // src/introspection.ts
14050
14761
  init_assert_field();
14762
+ init_db_type();
14051
14763
  import { ModelIRImpl } from "@dbsp/core";
14052
14764
  import { buildRelationKeyFields } from "@dbsp/types";
14053
14765
  function parseExpressionsList(raw) {
@@ -14076,6 +14788,7 @@ async function queryAllCatalogs(pool, schema) {
14076
14788
  pksResult,
14077
14789
  fksResult,
14078
14790
  indexesResult,
14791
+ uniqueColumnsResult,
14079
14792
  enumsResult,
14080
14793
  commentsResult,
14081
14794
  checksResult,
@@ -14083,7 +14796,8 @@ async function queryAllCatalogs(pool, schema) {
14083
14796
  extensionsResult,
14084
14797
  sequencesResult,
14085
14798
  rlsResult,
14086
- policiesResult
14799
+ policiesResult,
14800
+ formattedColumnTypesResult
14087
14801
  ] = await Promise.all([
14088
14802
  // 1. Columns (including identity and collation)
14089
14803
  pool.query(
@@ -14109,28 +14823,44 @@ async function queryAllCatalogs(pool, schema) {
14109
14823
  // 3. Foreign keys
14110
14824
  pool.query(
14111
14825
  `SELECT
14112
- tc.constraint_name,
14113
- kcu.table_name AS source_table,
14114
- kcu.column_name AS source_column,
14115
- ccu.table_name AS target_table,
14116
- ccu.column_name AS target_column,
14117
- rc.delete_rule,
14118
- rc.update_rule,
14119
- tc.is_deferrable,
14120
- tc.initially_deferred
14121
- FROM information_schema.table_constraints tc
14122
- JOIN information_schema.key_column_usage kcu
14123
- ON tc.constraint_name = kcu.constraint_name
14124
- AND tc.table_schema = kcu.table_schema
14125
- JOIN information_schema.constraint_column_usage ccu
14126
- ON ccu.constraint_name = tc.constraint_name
14127
- AND ccu.table_schema = tc.table_schema
14128
- JOIN information_schema.referential_constraints rc
14129
- ON rc.constraint_name = tc.constraint_name
14130
- AND rc.constraint_schema = tc.table_schema
14131
- WHERE tc.constraint_type = 'FOREIGN KEY'
14132
- AND tc.table_schema = $1
14133
- ORDER BY tc.constraint_name, kcu.ordinal_position`,
14826
+ c.conname AS constraint_name,
14827
+ source_rel.relname AS source_table,
14828
+ source_attr.attname AS source_column,
14829
+ target_ns.nspname AS target_schema,
14830
+ target_rel.relname AS target_table,
14831
+ target_attr.attname AS target_column,
14832
+ CASE c.confdeltype
14833
+ WHEN 'c' THEN 'CASCADE'
14834
+ WHEN 'n' THEN 'SET NULL'
14835
+ WHEN 'd' THEN 'SET DEFAULT'
14836
+ WHEN 'r' THEN 'RESTRICT'
14837
+ ELSE 'NO ACTION'
14838
+ END AS delete_rule,
14839
+ CASE c.confupdtype
14840
+ WHEN 'c' THEN 'CASCADE'
14841
+ WHEN 'n' THEN 'SET NULL'
14842
+ WHEN 'd' THEN 'SET DEFAULT'
14843
+ WHEN 'r' THEN 'RESTRICT'
14844
+ ELSE 'NO ACTION'
14845
+ END AS update_rule,
14846
+ CASE WHEN c.condeferrable THEN 'YES' ELSE 'NO' END AS is_deferrable,
14847
+ CASE WHEN c.condeferred THEN 'YES' ELSE 'NO' END AS initially_deferred
14848
+ FROM pg_constraint c
14849
+ JOIN pg_class source_rel ON source_rel.oid = c.conrelid
14850
+ JOIN pg_namespace source_ns ON source_ns.oid = source_rel.relnamespace
14851
+ JOIN pg_class target_rel ON target_rel.oid = c.confrelid
14852
+ JOIN pg_namespace target_ns ON target_ns.oid = target_rel.relnamespace
14853
+ JOIN LATERAL unnest(c.conkey, c.confkey) WITH ORDINALITY AS cols(source_attnum, target_attnum, ord) ON true
14854
+ JOIN pg_attribute source_attr
14855
+ ON source_attr.attrelid = c.conrelid
14856
+ AND source_attr.attnum = cols.source_attnum
14857
+ JOIN pg_attribute target_attr
14858
+ ON target_attr.attrelid = c.confrelid
14859
+ AND target_attr.attnum = cols.target_attnum
14860
+ WHERE c.contype = 'f'
14861
+ AND c.conparentid = 0
14862
+ AND source_ns.nspname = $1
14863
+ ORDER BY c.conname, cols.ord`,
14134
14864
  [schema]
14135
14865
  ),
14136
14866
  // 4. Indexes (excluding PK-backing indexes and unique-constraint-backing indexes).
@@ -14194,7 +14924,19 @@ async function queryAllCatalogs(pool, schema) {
14194
14924
  ORDER BY t.relname, i.relname`,
14195
14925
  [schema]
14196
14926
  ),
14197
- // 5. ENUM types
14927
+ // 5. Single-column UNIQUE constraints tracked as ColumnIR.unique
14928
+ pool.query(
14929
+ `SELECT rel.relname AS table_name, att.attname AS column_name, c.conname AS constraint_name
14930
+ FROM pg_constraint c
14931
+ JOIN pg_class rel ON rel.oid = c.conrelid
14932
+ JOIN pg_namespace ns ON ns.oid = rel.relnamespace
14933
+ JOIN pg_attribute att ON att.attrelid = c.conrelid AND att.attnum = c.conkey[1]
14934
+ WHERE c.contype = 'u'
14935
+ AND array_length(c.conkey, 1) = 1
14936
+ AND ns.nspname = $1`,
14937
+ [schema]
14938
+ ),
14939
+ // 6. ENUM types
14198
14940
  pool.query(
14199
14941
  `SELECT
14200
14942
  t.typname AS name,
@@ -14207,7 +14949,7 @@ async function queryAllCatalogs(pool, schema) {
14207
14949
  GROUP BY t.typname`,
14208
14950
  [schema]
14209
14951
  ),
14210
- // 6. Comments (table and column level) from pg_description
14952
+ // 7. Comments (table and column level) from pg_description
14211
14953
  pool.query(
14212
14954
  `SELECT
14213
14955
  c.relname AS table_name,
@@ -14221,7 +14963,7 @@ async function queryAllCatalogs(pool, schema) {
14221
14963
  AND d.objsubid >= 0`,
14222
14964
  [schema]
14223
14965
  ),
14224
- // 7. CHECK constraints
14966
+ // 8. CHECK constraints
14225
14967
  pool.query(
14226
14968
  `SELECT
14227
14969
  c.conname AS name,
@@ -14233,7 +14975,7 @@ async function queryAllCatalogs(pool, schema) {
14233
14975
  AND n.nspname = $1`,
14234
14976
  [schema]
14235
14977
  ),
14236
- // 8. Partition configurations
14978
+ // 9. Partition configurations
14237
14979
  pool.query(
14238
14980
  `SELECT
14239
14981
  c.relname AS table_name,
@@ -14248,13 +14990,13 @@ async function queryAllCatalogs(pool, schema) {
14248
14990
  GROUP BY c.relname, p.partstrat`,
14249
14991
  [schema]
14250
14992
  ),
14251
- // 9. Installed extensions (no schema param — queries globally; skip plpgsql)
14993
+ // 10. Installed extensions (no schema param — queries globally; skip plpgsql)
14252
14994
  pool.query(
14253
14995
  `SELECT extname AS name
14254
14996
  FROM pg_extension
14255
14997
  WHERE extname != 'plpgsql'`
14256
14998
  ),
14257
- // 10. Sequences not backed by SERIAL
14999
+ // 11. Sequences not backed by SERIAL
14258
15000
  pool.query(
14259
15001
  `SELECT s.sequencename AS name, s.start_value, s.increment_by, s.min_value, s.max_value, s.cycle
14260
15002
  FROM pg_sequences s
@@ -14265,7 +15007,7 @@ async function queryAllCatalogs(pool, schema) {
14265
15007
  AND d.objid IS NULL`,
14266
15008
  [schema]
14267
15009
  ),
14268
- // 11. RLS enabled state per table
15010
+ // 12. RLS enabled state per table
14269
15011
  pool.query(
14270
15012
  `SELECT c.relname AS table_name, c.relrowsecurity AS rls_enabled
14271
15013
  FROM pg_class c
@@ -14274,7 +15016,7 @@ async function queryAllCatalogs(pool, schema) {
14274
15016
  AND c.relkind = 'r'`,
14275
15017
  [schema]
14276
15018
  ),
14277
- // 12. Row-Level Security policies
15019
+ // 13. Row-Level Security policies
14278
15020
  pool.query(
14279
15021
  `SELECT
14280
15022
  c.relname AS table_name,
@@ -14289,6 +15031,29 @@ async function queryAllCatalogs(pool, schema) {
14289
15031
  JOIN pg_namespace n ON n.oid = c.relnamespace
14290
15032
  WHERE n.nspname = $1`,
14291
15033
  [schema]
15034
+ ),
15035
+ // 14. Faithful SQL-facing column types via format_type, for two cases:
15036
+ // (a) typmod-bearing columns (atttypmod <> -1) — varchar(120), numeric(10,2),
15037
+ // timestamptz(3), bit(8), vector(768) — to preserve modifier fidelity (#261);
15038
+ // (b) array columns (typcategory 'A') — so integer[] is stored as `integer[]`
15039
+ // rather than the internal `_int4` udt_name.
15040
+ // Other columns (plain base types, enums/composites/domains) fall back to
15041
+ // quoteTypeIdentifier(udt_name) in buildTableIR, which keeps a bare typname for
15042
+ // lowercase types and quotes a case-sensitive custom type (`"Money"`).
15043
+ pool.query(
15044
+ `SELECT c.relname AS table_name,
15045
+ a.attname AS column_name,
15046
+ format_type(a.atttypid, a.atttypmod) AS db_type
15047
+ FROM pg_catalog.pg_attribute a
15048
+ JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
15049
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
15050
+ JOIN pg_catalog.pg_type t ON t.oid = a.atttypid
15051
+ WHERE n.nspname = $1
15052
+ AND c.relkind IN ('r','p','v','m','f')
15053
+ AND a.attnum > 0
15054
+ AND NOT a.attisdropped
15055
+ AND (a.atttypmod <> -1 OR t.typcategory = 'A')`,
15056
+ [schema]
14292
15057
  )
14293
15058
  ]);
14294
15059
  return {
@@ -14296,6 +15061,7 @@ async function queryAllCatalogs(pool, schema) {
14296
15061
  pks: pksResult.rows,
14297
15062
  fks: fksResult.rows,
14298
15063
  indexes: indexesResult.rows,
15064
+ uniqueColumns: uniqueColumnsResult.rows,
14299
15065
  enums: enumsResult.rows,
14300
15066
  comments: commentsResult.rows,
14301
15067
  checks: checksResult.rows,
@@ -14303,7 +15069,8 @@ async function queryAllCatalogs(pool, schema) {
14303
15069
  extensions: extensionsResult.rows,
14304
15070
  sequences: sequencesResult.rows,
14305
15071
  rls: rlsResult.rows,
14306
- policies: policiesResult.rows
15072
+ policies: policiesResult.rows,
15073
+ formattedColumnTypes: formattedColumnTypesResult.rows
14307
15074
  };
14308
15075
  }
14309
15076
  function buildPartitionMap(rows) {
@@ -14381,6 +15148,21 @@ function buildIndexMap(rows) {
14381
15148
  }
14382
15149
  return result;
14383
15150
  }
15151
+ function buildUniqueColumnMap(rows) {
15152
+ const result = /* @__PURE__ */ new Map();
15153
+ for (const row of rows) {
15154
+ const existing = result.get(row.table_name);
15155
+ if (existing) {
15156
+ existing.set(row.column_name, row.constraint_name);
15157
+ } else {
15158
+ result.set(
15159
+ row.table_name,
15160
+ /* @__PURE__ */ new Map([[row.column_name, row.constraint_name]])
15161
+ );
15162
+ }
15163
+ }
15164
+ return result;
15165
+ }
14384
15166
  function buildColumnMap(rows) {
14385
15167
  const result = /* @__PURE__ */ new Map();
14386
15168
  for (const col of rows) {
@@ -14408,14 +15190,16 @@ function buildPKMap(rows) {
14408
15190
  function buildFKMap(rows) {
14409
15191
  const result = /* @__PURE__ */ new Map();
14410
15192
  for (const fk of rows) {
14411
- const existing = result.get(fk.constraint_name);
15193
+ const key = JSON.stringify([fk.source_table, fk.constraint_name]);
15194
+ const existing = result.get(key);
14412
15195
  if (existing) {
14413
15196
  existing.cols.push(fk.source_column);
14414
15197
  existing.refs.push(fk.target_column);
14415
15198
  } else {
14416
- result.set(fk.constraint_name, {
15199
+ result.set(key, {
14417
15200
  source: fk.source_table,
14418
15201
  target: fk.target_table,
15202
+ targetSchema: fk.target_schema,
14419
15203
  cols: [fk.source_column],
14420
15204
  refs: [fk.target_column],
14421
15205
  deleteRule: fk.delete_rule,
@@ -14478,13 +15262,25 @@ function buildRLSMaps(rlsRows, policiesRows) {
14478
15262
  }
14479
15263
  return { tableRlsEnabled, tablePolicies };
14480
15264
  }
15265
+ function columnTypeMapKey(tableName, columnName) {
15266
+ return `${tableName}\0${columnName}`;
15267
+ }
15268
+ function buildFormattedColumnTypeMap(rows) {
15269
+ const result = /* @__PURE__ */ new Map();
15270
+ for (const row of rows) {
15271
+ result.set(columnTypeMapKey(row.table_name, row.column_name), row.db_type);
15272
+ }
15273
+ return result;
15274
+ }
14481
15275
  function buildTableIR(tableName, ctx) {
14482
15276
  const rawCols = ctx.tableColumns.get(tableName) ?? [];
14483
15277
  const pkCols = ctx.tablePKs.get(tableName);
15278
+ const uniqueColumns = ctx.uniqueColumns.get(tableName);
14484
15279
  const columns = rawCols.map((col) => {
14485
15280
  const identity = col.is_identity === "YES" && col.identity_generation ? col.identity_generation === "ALWAYS" ? "always" : "byDefault" : void 0;
14486
15281
  const collation = col.collation_name && col.collation_name !== "default" ? col.collation_name : void 0;
14487
15282
  const colComment = ctx.columnComments.get(`${tableName}.${col.column_name}`) ?? void 0;
15283
+ const uniqueConstraintName = uniqueColumns?.get(col.column_name);
14488
15284
  return {
14489
15285
  name: col.column_name,
14490
15286
  type: mapPgType(col.data_type, col.udt_name),
@@ -14493,7 +15289,14 @@ function buildTableIR(tableName, ctx) {
14493
15289
  // it verbatim (e.g. CURRENT_TIMESTAMP, nextval('seq'::regclass)) instead
14494
15290
  // of quoting it as a string literal.
14495
15291
  ...col.column_default != null ? { default: { sql: col.column_default } } : {},
14496
- originalDbType: col.udt_name,
15292
+ // format_type output (typmod-bearing + array columns) is already
15293
+ // SQL-facing; the udt_name fallback is a bare catalog typname, so
15294
+ // quote it when case-sensitive (`"Money"`) — a lowercase typname
15295
+ // stays bare, preserving the common enum/UDT identity.
15296
+ originalDbType: ctx.formattedColumnTypes.get(
15297
+ columnTypeMapKey(tableName, col.column_name)
15298
+ ) ?? quoteTypeIdentifier(col.udt_name),
15299
+ ...uniqueConstraintName !== void 0 ? { unique: true, uniqueConstraintName } : {},
14497
15300
  ...collation ? { collation } : {},
14498
15301
  ...identity ? { identity } : {},
14499
15302
  ...colComment ? { comment: colComment } : {}
@@ -14502,12 +15305,17 @@ function buildTableIR(tableName, ctx) {
14502
15305
  const foreignKeys = [];
14503
15306
  for (const [, fk] of ctx.fksByConstraint) {
14504
15307
  if (fk.source !== tableName) continue;
14505
- if (!ctx.tableNames.includes(fk.target)) continue;
15308
+ const isCrossSchema = fk.targetSchema !== void 0 && fk.targetSchema !== ctx.schema;
15309
+ if (!isCrossSchema && !ctx.tableNames.includes(fk.target)) continue;
14506
15310
  const onDelete = mapDeleteRule(fk.deleteRule);
14507
15311
  const onUpdate = mapDeleteRule(fk.updateRule);
14508
15312
  foreignKeys.push({
14509
15313
  columns: fk.cols,
14510
- references: { table: fk.target, columns: fk.refs },
15314
+ references: {
15315
+ table: fk.target,
15316
+ columns: fk.refs,
15317
+ ...isCrossSchema ? { schema: fk.targetSchema } : {}
15318
+ },
14511
15319
  ...onDelete !== "NO ACTION" ? { onDelete } : {},
14512
15320
  ...onUpdate !== "NO ACTION" ? { onUpdate } : {},
14513
15321
  ...fk.deferred ? { deferred: true } : {}
@@ -14555,7 +15363,11 @@ async function introspect(pool, options) {
14555
15363
  const tablePartitions = buildPartitionMap(raw.partitions);
14556
15364
  const tableChecks = buildCheckMap(raw.checks);
14557
15365
  const tableIndexes = buildIndexMap(raw.indexes);
15366
+ const uniqueColumns = buildUniqueColumnMap(raw.uniqueColumns);
14558
15367
  const tableColumns = buildColumnMap(raw.columns);
15368
+ const formattedColumnTypes = buildFormattedColumnTypeMap(
15369
+ raw.formattedColumnTypes
15370
+ );
14559
15371
  const tablePKs = buildPKMap(raw.pks);
14560
15372
  const fksByConstraint = buildFKMap(raw.fks);
14561
15373
  const enumMap = buildEnumMap(raw.enums);
@@ -14570,9 +15382,11 @@ async function introspect(pool, options) {
14570
15382
  for (const tableName of tableNames) {
14571
15383
  const table = buildTableIR(tableName, {
14572
15384
  tableColumns,
15385
+ formattedColumnTypes,
14573
15386
  tablePKs,
14574
15387
  fksByConstraint,
14575
15388
  tableIndexes,
15389
+ uniqueColumns,
14576
15390
  tableChecks,
14577
15391
  tableComments,
14578
15392
  columnComments,
@@ -14580,20 +15394,33 @@ async function introspect(pool, options) {
14580
15394
  tableRlsEnabled,
14581
15395
  tablePolicies,
14582
15396
  tableNames,
15397
+ schema,
14583
15398
  warnings
14584
15399
  });
14585
15400
  tables.set(tableName, table);
14586
15401
  }
14587
15402
  const extensions = raw.extensions.map((r) => r.name);
14588
15403
  const sequenceMap = buildSequenceMap(raw.sequences);
14589
- const relations = inferRelations(fksByConstraint, tableNames);
14590
- const hierarchies = detectHierarchies(tables, fksByConstraint, tableNames);
15404
+ const relations = inferRelations(fksByConstraint, tableNames, schema);
15405
+ const hierarchies = detectHierarchies(
15406
+ tables,
15407
+ fksByConstraint,
15408
+ tableNames,
15409
+ schema
15410
+ );
15411
+ const externalTables = /* @__PURE__ */ new Set();
15412
+ for (const fk of fksByConstraint.values()) {
15413
+ if (fk.targetSchema !== void 0 && fk.targetSchema !== schema && !tables.has(fk.target)) {
15414
+ externalTables.add(fk.target);
15415
+ }
15416
+ }
14591
15417
  const modelIR = new ModelIRImpl(
14592
15418
  tables,
14593
15419
  relations,
14594
15420
  enumMap,
14595
15421
  extensions,
14596
- sequenceMap
15422
+ sequenceMap,
15423
+ externalTables
14597
15424
  );
14598
15425
  return Object.assign(modelIR, {
14599
15426
  hierarchies,
@@ -14601,10 +15428,11 @@ async function introspect(pool, options) {
14601
15428
  warnings
14602
15429
  });
14603
15430
  }
14604
- function inferRelations(fksByConstraint, filteredTables) {
15431
+ function inferRelations(fksByConstraint, filteredTables, schema) {
14605
15432
  const relations = /* @__PURE__ */ new Map();
14606
15433
  const filteredSet = new Set(filteredTables);
14607
15434
  for (const [, fk] of fksByConstraint) {
15435
+ if (fk.targetSchema !== void 0 && fk.targetSchema !== schema) continue;
14608
15436
  if (!filteredSet.has(fk.source) || !filteredSet.has(fk.target)) continue;
14609
15437
  const belongsToName = deriveRelationName(fk.cols[0], fk.target);
14610
15438
  const relationFk = {
@@ -14654,13 +15482,14 @@ function deriveRelationName(fkColumn, _targetTable) {
14654
15482
  }
14655
15483
  return fkColumn;
14656
15484
  }
14657
- function detectHierarchies(tables, fksByConstraint, filteredTables) {
15485
+ function detectHierarchies(tables, fksByConstraint, filteredTables, schema) {
14658
15486
  const hierarchies = [];
14659
15487
  const filteredSet = new Set(filteredTables);
14660
15488
  const fksBySourceTarget = /* @__PURE__ */ new Map();
14661
15489
  for (const [, fk] of fksByConstraint) {
15490
+ if (fk.targetSchema !== void 0 && fk.targetSchema !== schema) continue;
14662
15491
  if (!filteredSet.has(fk.source) || !filteredSet.has(fk.target)) continue;
14663
- if (fk.source === fk.target && fk.cols.length === 1) {
15492
+ if (fk.source === fk.target && fk.cols.length === 1 && (fk.targetSchema === void 0 || fk.targetSchema === schema)) {
14664
15493
  const table = tables.get(fk.source);
14665
15494
  const pk = table?.primaryKey;
14666
15495
  const nodeIdColumn = typeof pk === "string" ? pk : pk?.[0] ?? DEFAULT_PK_COLUMN;
@@ -15601,11 +16430,7 @@ init_naming_plugin();
15601
16430
  init_param_ref();
15602
16431
 
15603
16432
  // src/pgsql-adapter.ts
15604
- import {
15605
- POSTGRESQL_CAPABILITIES as POSTGRESQL_CAPABILITIES2,
15606
- plan as planFn2,
15607
- validateTypeName as validateTypeName4
15608
- } from "@dbsp/core";
16433
+ import { POSTGRESQL_CAPABILITIES as POSTGRESQL_CAPABILITIES2, plan as planFn2 } from "@dbsp/core";
15609
16434
  import { getNqlBindingRefName as getNqlBindingRefName2, isNqlBindingRef as isNqlBindingRef2 } from "@dbsp/types/internal";
15610
16435
 
15611
16436
  // src/adapter-compiler-includes.ts
@@ -15825,6 +16650,7 @@ init_compile_where();
15825
16650
  import { countDistinctRelationPathsByName } from "@dbsp/core";
15826
16651
  import { toColumnList as toColumnList10 } from "@dbsp/types";
15827
16652
  init_compiler_utils();
16653
+ init_db_type();
15828
16654
  init_types();
15829
16655
  init_intent_to_decisions();
15830
16656
  init_param_ref();
@@ -16727,16 +17553,6 @@ function buildIncludeTree(allDecisions) {
16727
17553
  }
16728
17554
 
16729
17555
  // src/adapter-compiler-select.ts
16730
- var ADAPTER_MULTIWORD_BASE_TYPES = [
16731
- "timestamp with time zone",
16732
- "timestamp without time zone",
16733
- "time with time zone",
16734
- "time without time zone",
16735
- "double precision",
16736
- "character varying",
16737
- "bit varying"
16738
- ];
16739
- var ADAPTER_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
16740
17556
  function assertSafeTypeName(typeName, colIndex) {
16741
17557
  const raw = typeName.trim();
16742
17558
  if (raw.length === 0) {
@@ -16744,42 +17560,14 @@ function assertSafeTypeName(typeName, colIndex) {
16744
17560
  `BatchValues compile error: type name at column index ${colIndex} must not be empty.`
16745
17561
  );
16746
17562
  }
16747
- let rest = raw;
16748
- if (rest.endsWith("[]")) {
16749
- rest = rest.slice(0, -2);
16750
- if (rest.endsWith("[]")) {
16751
- throw new Error(
16752
- `BatchValues compile error: unsafe type name '${typeName}' at column index ${colIndex}. At most one array suffix "[]" is allowed as a raw type-name input.`
16753
- );
16754
- }
16755
- }
16756
- const modifierMatch = rest.match(/\(([^)]*)\)$/);
16757
- if (modifierMatch) {
16758
- const inner = modifierMatch[1] ?? "";
16759
- if (!/^\d+(?:,\d+)?$/.test(inner)) {
16760
- throw new Error(
16761
- `BatchValues compile error: unsafe type name '${typeName}' at column index ${colIndex}. Type modifier must be "(N)" or "(N,M)" with digits only; got "(${inner})".`
16762
- );
16763
- }
16764
- rest = rest.slice(0, rest.length - modifierMatch[0].length).trimEnd();
16765
- }
16766
- const baseLower = rest.toLowerCase();
16767
- if (ADAPTER_MULTIWORD_BASE_TYPES.includes(baseLower)) {
16768
- return;
16769
- }
16770
- const parts = rest.split(".");
16771
- if (parts.length > 2) {
17563
+ try {
17564
+ validateDbType(raw);
17565
+ } catch (error) {
17566
+ const reason = error instanceof Error ? ` ${error.message}` : "";
16772
17567
  throw new Error(
16773
- `BatchValues compile error: unsafe type name '${typeName}' at column index ${colIndex}. Schema-qualified types allow at most one dot (schema.type).`
17568
+ `BatchValues compile error: unsafe type name '${typeName}' at column index ${colIndex}.${reason}`
16774
17569
  );
16775
17570
  }
16776
- for (const part of parts) {
16777
- if (!ADAPTER_IDENT_RE.test(part)) {
16778
- throw new Error(
16779
- `BatchValues compile error: unsafe type name '${typeName}' at column index ${colIndex}. Base type "${part}" is not a valid SQL identifier and is not in the multi-word type allowlist.`
16780
- );
16781
- }
16782
- }
16783
17571
  }
16784
17572
  function buildBatchValuesRangeFn(bv, startParamIndex, aliasOverride) {
16785
17573
  const params = [];
@@ -16935,14 +17723,16 @@ function compileJoinIntents(joins, rootTable, schemaName, deps) {
16935
17723
  schemaForFromName(schemaName, intent.table, deps.bindingNames, naming),
16936
17724
  naming
16937
17725
  );
16938
- results.push({
17726
+ const joinDecision = {
16939
17727
  type: "join",
16940
17728
  targetTable: intent.table,
16941
17729
  alias: tableAlias,
16942
17730
  joinType: intent.type,
16943
17731
  joinRarg: joinedRangeVar,
16944
- joinOnNode: onNode
16945
- });
17732
+ joinOnNode: onNode,
17733
+ joinOnParams: paramState.parameters
17734
+ };
17735
+ results.push(joinDecision);
16946
17736
  }
16947
17737
  }
16948
17738
  return results;
@@ -17240,6 +18030,7 @@ function compileWithIncludes(plan, options, deps) {
17240
18030
  init_binding_registry();
17241
18031
  init_compile_where();
17242
18032
  init_compiler_utils();
18033
+ init_db_type();
17243
18034
  init_handlers();
17244
18035
  init_cte_compiler();
17245
18036
  function whereIntentAsDecision(where) {
@@ -17352,7 +18143,14 @@ function getColumnTypes(tableName, columns, deps) {
17352
18143
  const columnIR = table.columns.find((c) => c.name === col);
17353
18144
  if (columnIR) {
17354
18145
  result ??= {};
17355
- result[col] = columnIR.originalDbType ?? columnIR.type;
18146
+ const authored = columnIR.originalDbType?.trim();
18147
+ const castTarget = authored !== void 0 && authored !== "" ? dbTypeCastTarget(validateDbType(authored)) : columnIR.type;
18148
+ if (castTarget.trim().endsWith("[]")) {
18149
+ throw new Error(
18150
+ `Batch mutation of array-typed column '${col}' (${castTarget}) is not supported: unnest flattens multi-dimensional arrays. Use single-row mutations for array columns.`
18151
+ );
18152
+ }
18153
+ result[col] = castTarget;
17356
18154
  }
17357
18155
  }
17358
18156
  return result;
@@ -18139,6 +18937,7 @@ function buildRecursiveAnchorWhere(where, tableAlias, deps, state) {
18139
18937
  init_assert_field();
18140
18938
  init_ast_helpers();
18141
18939
  init_binding_registry();
18940
+ init_db_type();
18142
18941
 
18143
18942
  // src/ddl/index-operations.ts
18144
18943
  init_validate();
@@ -18149,8 +18948,9 @@ function qualifyTable3(table, schema) {
18149
18948
  return schema ? `${quoteIdent2(schema, "schema")}.${quoteIdent2(table, "table")}` : quoteIdent2(table, "table");
18150
18949
  }
18151
18950
  function generateCreateIndexSQL(table, options, schema) {
18152
- if (options.method !== void 0) {
18153
- validateIndexMethod(options.method, "index method");
18951
+ const indexMethod = options.method;
18952
+ if (indexMethod !== void 0) {
18953
+ validateIndexMethod(indexMethod, "index method");
18154
18954
  }
18155
18955
  const parts = ["CREATE"];
18156
18956
  if (options.unique) parts.push("UNIQUE");
@@ -18159,8 +18959,8 @@ function generateCreateIndexSQL(table, options, schema) {
18159
18959
  if (options.ifNotExists) parts.push("IF NOT EXISTS");
18160
18960
  parts.push(quoteIdentifier2(options.name));
18161
18961
  parts.push(`ON ${qualifyTable3(table, schema)}`);
18162
- if (options.method) {
18163
- parts.push(`USING ${options.method}`);
18962
+ if (indexMethod) {
18963
+ parts.push(`USING ${indexMethod}`);
18164
18964
  }
18165
18965
  const colParts = [];
18166
18966
  for (const col of options.columns) {
@@ -18177,13 +18977,14 @@ function generateCreateIndexSQL(table, options, schema) {
18177
18977
  if (options.with && Object.keys(options.with).length > 0) {
18178
18978
  const withParams = Object.entries(options.with).map(([k, v]) => {
18179
18979
  validateIdentifier(k, "alias");
18180
- return `${k} = ${v}`;
18980
+ return `${k} = ${formatStorageParameterValue(v, `index WITH parameter "${k}"`)}`;
18181
18981
  }).join(", ");
18182
18982
  parts.push(`WITH (${withParams})`);
18183
18983
  }
18184
- if (options.where) {
18185
- validateSqlExpression(options.where, "index WHERE predicate");
18186
- parts.push(`WHERE ${options.where}`);
18984
+ const whereExpr = options.where;
18985
+ if (whereExpr) {
18986
+ validateSqlExpression(whereExpr, "index WHERE predicate");
18987
+ parts.push(`WHERE ${whereExpr}`);
18187
18988
  }
18188
18989
  return parts.join(" ");
18189
18990
  }
@@ -18194,10 +18995,11 @@ function buildColumnPart(col, opclassMap) {
18194
18995
  if (opclass2) validateIdentifier(opclass2, "alias");
18195
18996
  return opclass2 ? `${quoted} ${opclass2}` : quoted;
18196
18997
  }
18197
- validateSqlExpression(col.expression, "index expression");
18998
+ const expression = col.expression;
18999
+ validateSqlExpression(expression, "index expression");
18198
19000
  const opclass = col.opclass;
18199
19001
  if (opclass) validateIdentifier(opclass, "alias");
18200
- return opclass ? `${col.expression} ${opclass}` : col.expression;
19002
+ return opclass ? `${expression} ${opclass}` : expression;
18201
19003
  }
18202
19004
  function generateDropIndexSQL(name, options) {
18203
19005
  const parts = ["DROP INDEX"];
@@ -18589,7 +19391,8 @@ function resolveRuntimeBindingColumnType(bindingName, sourceTable, columnName) {
18589
19391
  `NQL runtime binding '${bindingName}' cannot resolve projected column '${columnName}' on source table '${sourceTable.name}'.`
18590
19392
  );
18591
19393
  }
18592
- const dbType = column.originalDbType?.trim() || mapRuntimeBindingColumnType(column.type);
19394
+ const originalDbType = column.originalDbType?.trim();
19395
+ const dbType = originalDbType || mapRuntimeBindingColumnType(column.type);
18593
19396
  if (dbType === void 0 || dbType.trim() === "") {
18594
19397
  throw new Error(
18595
19398
  `NQL runtime binding '${bindingName}' cannot resolve a PostgreSQL type for projected column '${columnName}' on source table '${sourceTable.name}'.`
@@ -18597,14 +19400,14 @@ function resolveRuntimeBindingColumnType(bindingName, sourceTable, columnName) {
18597
19400
  }
18598
19401
  const typeName = dbType.trim();
18599
19402
  try {
18600
- validateTypeName4(typeName);
19403
+ validateDbType(typeName);
18601
19404
  } catch (error) {
18602
19405
  const reason = error instanceof Error ? error.message : String(error);
18603
19406
  throw new Error(
18604
19407
  `NQL runtime binding '${bindingName}' cannot use PostgreSQL cast type for projected column '${columnName}': ${reason}`
18605
19408
  );
18606
19409
  }
18607
- return typeName;
19410
+ return originalDbType !== void 0 ? dbTypeCastTarget(typeName) : typeName;
18608
19411
  }
18609
19412
  function resolveRuntimeBindingColumnTypes(name, binding, model, sourceTableName) {
18610
19413
  if (model === void 0) {
@@ -18638,7 +19441,8 @@ function resolvePgTypeForColumnTypeInfo(bindingName, column, info) {
18638
19441
  `NQL runtime binding '${bindingName}' cannot resolve a PostgreSQL type for projected column '${column}': unsupported aggregate kind '${String(info.fn)}'.`
18639
19442
  );
18640
19443
  }
18641
- const rawType = info.kind === "aggregate" ? "bigint" : info.originalDbType?.trim() || mapRuntimeBindingColumnType(info.type);
19444
+ const originalDbType = info.kind === "aggregate" ? void 0 : info.originalDbType?.trim();
19445
+ const rawType = info.kind === "aggregate" ? "bigint" : originalDbType || mapRuntimeBindingColumnType(info.type);
18642
19446
  if (rawType === void 0 || rawType.trim() === "") {
18643
19447
  throw new Error(
18644
19448
  `NQL runtime binding '${bindingName}' cannot resolve a PostgreSQL type for projected column '${column}'.`
@@ -18646,14 +19450,14 @@ function resolvePgTypeForColumnTypeInfo(bindingName, column, info) {
18646
19450
  }
18647
19451
  const typeName = rawType.trim();
18648
19452
  try {
18649
- validateTypeName4(typeName);
19453
+ validateDbType(typeName);
18650
19454
  } catch (error) {
18651
19455
  const reason = error instanceof Error ? error.message : String(error);
18652
19456
  throw new Error(
18653
19457
  `NQL runtime binding '${bindingName}' cannot use PostgreSQL cast type for projected column '${column}': ${reason}`
18654
19458
  );
18655
19459
  }
18656
- return typeName;
19460
+ return originalDbType !== void 0 ? dbTypeCastTarget(typeName) : typeName;
18657
19461
  }
18658
19462
  function resolveRuntimeBindingCteColumnTypes(name, binding) {
18659
19463
  const columnTypes = binding.columnTypes;