@dbsp/adapter-pgsql 1.10.2 → 1.11.1

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
@@ -95,7 +95,46 @@ var init_naming_plugin = __esm({
95
95
  });
96
96
 
97
97
  // src/validate.ts
98
+ function assertString(value, context) {
99
+ if (typeof value !== "string") {
100
+ throw new Error(`${context}: expected a string, received ${typeof value}`);
101
+ }
102
+ }
103
+ function assertNumericLiteral(value, context) {
104
+ if (typeof value !== "number" && typeof value !== "string") {
105
+ throw new Error(
106
+ `${context}: expected a finite number or numeric string, received ${typeof value}`
107
+ );
108
+ }
109
+ const literal4 = typeof value === "number" ? String(value) : value;
110
+ if (typeof value === "number" && !Number.isFinite(value) || !SAFE_DECIMAL_LITERAL.test(literal4)) {
111
+ throw new Error(`${context}: unsafe numeric literal "${literal4}"`);
112
+ }
113
+ return literal4;
114
+ }
115
+ function formatStorageParameterValue(value, context) {
116
+ if (typeof value !== "number" && typeof value !== "string") {
117
+ throw new Error(
118
+ `${context}: expected a string or number, received ${typeof value}`
119
+ );
120
+ }
121
+ if (typeof value === "number") {
122
+ return assertNumericLiteral(value, context);
123
+ }
124
+ if (value.includes("\0")) {
125
+ throw new Error(`${context}: contains null byte`);
126
+ }
127
+ if (SAFE_DECIMAL_LITERAL.test(value) || SAFE_STORAGE_BOOLEAN_LITERAL.test(value) || SAFE_SIMPLE_IDENTIFIER_VALUE.test(value) && !isReservedKeyword(value) && value.toLowerCase() !== "null") {
128
+ return value;
129
+ }
130
+ return `'${value.replace(/'/g, "''")}'`;
131
+ }
98
132
  function validateIdentifier(value, type) {
133
+ if (typeof value !== "string") {
134
+ throw new Error(
135
+ `Invalid ${type} identifier: expected a string, received ${typeof value}`
136
+ );
137
+ }
99
138
  if (!value || value.length === 0) {
100
139
  throw new InvalidIdentifierError(value, type, "cannot be empty");
101
140
  }
@@ -171,7 +210,12 @@ function validateIdentifiers(identifiers) {
171
210
  function sanitizeForDisplay(value) {
172
211
  return value.replace(/[\x00-\x1f\x7f]/g, "?").slice(0, 100);
173
212
  }
213
+ function sanitizeCommentText(value) {
214
+ assertString(value, "SQL comment text");
215
+ return value.replace(/[\x00-\x1f\x7f]/g, "");
216
+ }
174
217
  function validateExtensionName(name, context = "extension") {
218
+ assertString(name, `Invalid ${context} identifier`);
175
219
  if (!name || name.length === 0) {
176
220
  throw new InvalidIdentifierError(name, context, "cannot be empty");
177
221
  }
@@ -257,6 +301,7 @@ function validateExtensionName(name, context = "extension") {
257
301
  }
258
302
  }
259
303
  function validateCollationName(name, context = "collation") {
304
+ assertString(name, `Invalid ${context} identifier`);
260
305
  if (!name || name.length === 0) {
261
306
  throw new InvalidIdentifierError(name, context, "cannot be empty");
262
307
  }
@@ -349,13 +394,106 @@ function validateCollationName(name, context = "collation") {
349
394
  }
350
395
  }
351
396
  function validateSqlExpression(sql, context) {
397
+ assertString(sql, `Unsafe SQL expression in ${context}`);
352
398
  if (/[;]|--|\/\*|\*\/|\$\$|\\/.test(sql)) {
353
399
  throw new Error(
354
400
  `Unsafe SQL expression in ${context}: contains forbidden characters (;, --, /*, */, "$$" (dollar-quoted strings), \\). Value: "${sql}"`
355
401
  );
356
402
  }
357
403
  }
404
+ function validateCheckExpression(sql, context) {
405
+ if (typeof sql !== "string") {
406
+ throw new Error(
407
+ `Unsafe SQL expression in ${context}: expected a string, received ${typeof sql}`
408
+ );
409
+ }
410
+ let i = 0;
411
+ let singleQuoted = false;
412
+ let dollarQuoteDelimiter;
413
+ while (i < sql.length) {
414
+ if (singleQuoted) {
415
+ if (sql[i] === "'") {
416
+ if (sql[i + 1] === "'") {
417
+ i += 2;
418
+ continue;
419
+ }
420
+ singleQuoted = false;
421
+ }
422
+ i++;
423
+ continue;
424
+ }
425
+ if (dollarQuoteDelimiter) {
426
+ if (sql.startsWith(dollarQuoteDelimiter, i)) {
427
+ i += dollarQuoteDelimiter.length;
428
+ dollarQuoteDelimiter = void 0;
429
+ continue;
430
+ }
431
+ i++;
432
+ continue;
433
+ }
434
+ if (sql[i] === "'") {
435
+ singleQuoted = true;
436
+ i++;
437
+ continue;
438
+ }
439
+ const dollarDelimiter = readDollarQuoteDelimiter(sql, i);
440
+ if (dollarDelimiter) {
441
+ dollarQuoteDelimiter = dollarDelimiter;
442
+ i += dollarDelimiter.length;
443
+ continue;
444
+ }
445
+ const forbiddenToken = forbiddenCheckTokenAt(sql, i);
446
+ if (forbiddenToken) {
447
+ throw new Error(
448
+ `Unsafe SQL expression in ${context}: contains forbidden token "${forbiddenToken}" outside string literal. Value: "${sql}"`
449
+ );
450
+ }
451
+ i++;
452
+ }
453
+ if (singleQuoted) {
454
+ throw new Error(
455
+ `Unsafe SQL expression in ${context}: unterminated single-quoted string literal. Value: "${sql}"`
456
+ );
457
+ }
458
+ if (dollarQuoteDelimiter) {
459
+ throw new Error(
460
+ `Unsafe SQL expression in ${context}: unterminated dollar-quoted string literal ${dollarQuoteDelimiter}. Value: "${sql}"`
461
+ );
462
+ }
463
+ }
464
+ function forbiddenCheckTokenAt(sql, index) {
465
+ const ch = sql[index];
466
+ if (ch === ";" || ch === "\\") return ch;
467
+ const pair = sql.slice(index, index + 2);
468
+ if (pair === "--" || pair === "/*" || pair === "*/") return pair;
469
+ return void 0;
470
+ }
471
+ function readDollarQuoteDelimiter(sql, index) {
472
+ if (sql[index] !== "$") return void 0;
473
+ const previous = index > 0 ? sql[index - 1] : void 0;
474
+ if (previous && isIdentifierContinuationBeforeDollarQuote(previous)) {
475
+ return void 0;
476
+ }
477
+ if (sql[index + 1] === "$") return "$$";
478
+ if (!isDollarQuoteTagStart(sql[index + 1])) return void 0;
479
+ let cursor = index + 2;
480
+ while (isDollarQuoteTagContinuation(sql[cursor])) {
481
+ cursor++;
482
+ }
483
+ if (sql[cursor] !== "$") return void 0;
484
+ return sql.slice(index, cursor + 1);
485
+ }
486
+ function isIdentifierContinuationBeforeDollarQuote(ch) {
487
+ return /[A-Za-z0-9_$]/.test(ch) || (ch.codePointAt(0) ?? 0) > 127;
488
+ }
489
+ function isDollarQuoteTagStart(ch) {
490
+ return ch !== void 0 && /[A-Za-z_]/.test(ch);
491
+ }
492
+ function isDollarQuoteTagContinuation(ch) {
493
+ return ch !== void 0 && /[A-Za-z0-9_]/.test(ch);
494
+ }
358
495
  function validateDbTypeName(type) {
496
+ assertString(type, "Unsafe database type name");
359
497
  if (!SAFE_TYPE_PATTERN.test(type)) {
360
498
  throw new Error(
361
499
  `Unsafe database type name: "${type}". Must match PostgreSQL type name rules.`
@@ -363,7 +501,7 @@ function validateDbTypeName(type) {
363
501
  }
364
502
  return type;
365
503
  }
366
- var SQL_RESERVED_KEYWORDS, InvalidIdentifierError, SAFE_TYPE_PATTERN;
504
+ var SQL_RESERVED_KEYWORDS, InvalidIdentifierError, SAFE_DECIMAL_LITERAL, SAFE_STORAGE_BOOLEAN_LITERAL, SAFE_SIMPLE_IDENTIFIER_VALUE, SAFE_TYPE_PATTERN;
367
505
  var init_validate = __esm({
368
506
  "src/validate.ts"() {
369
507
  "use strict";
@@ -482,6 +620,9 @@ var init_validate = __esm({
482
620
  identifierType;
483
621
  reason;
484
622
  };
623
+ SAFE_DECIMAL_LITERAL = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/;
624
+ SAFE_STORAGE_BOOLEAN_LITERAL = /^(?:true|false|on|off)$/i;
625
+ SAFE_SIMPLE_IDENTIFIER_VALUE = /^[A-Za-z_][A-Za-z0-9_]*$/;
485
626
  SAFE_TYPE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_ ]*(\(\d+(,\s*\d+)?\))?(\[\])?$/;
486
627
  }
487
628
  });
@@ -1126,7 +1267,7 @@ var init_param_value = __esm({
1126
1267
  });
1127
1268
 
1128
1269
  // src/handlers/expression/case-value.ts
1129
- function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler) {
1270
+ function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler, expressionHandler) {
1130
1271
  if (value === null || value === void 0) {
1131
1272
  return nullConstNode();
1132
1273
  }
@@ -1160,7 +1301,8 @@ function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler
1160
1301
  schema,
1161
1302
  naming,
1162
1303
  state,
1163
- nestedCaseHandler
1304
+ nestedCaseHandler,
1305
+ expressionHandler
1164
1306
  );
1165
1307
  const right = resolveCaseValue(
1166
1308
  expr.right,
@@ -1168,7 +1310,8 @@ function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler
1168
1310
  schema,
1169
1311
  naming,
1170
1312
  state,
1171
- nestedCaseHandler
1313
+ nestedCaseHandler,
1314
+ expressionHandler
1172
1315
  );
1173
1316
  return {
1174
1317
  A_Expr: {
@@ -1186,15 +1329,29 @@ function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler
1186
1329
  }
1187
1330
  // falls through
1188
1331
  default: {
1332
+ if (expressionHandler && typeof expr.kind === "string" && EXPRESSION_HANDLER_KINDS.has(expr.kind)) {
1333
+ return expressionHandler(expr);
1334
+ }
1189
1335
  return bindParameter(value, state);
1190
1336
  }
1191
1337
  }
1192
1338
  }
1339
+ var EXPRESSION_HANDLER_KINDS;
1193
1340
  var init_case_value = __esm({
1194
1341
  "src/handlers/expression/case-value.ts"() {
1195
1342
  "use strict";
1196
1343
  init_ast_helpers();
1197
1344
  init_param_value();
1345
+ EXPRESSION_HANDLER_KINDS = /* @__PURE__ */ new Set([
1346
+ "customOp",
1347
+ "customFn",
1348
+ "ref",
1349
+ "cast",
1350
+ "unary",
1351
+ "array",
1352
+ "subquery",
1353
+ "relationColumn"
1354
+ ]);
1198
1355
  }
1199
1356
  });
1200
1357
 
@@ -6659,7 +6816,21 @@ var init_arithmetic = __esm({
6659
6816
  // src/handlers/expression/case.ts
6660
6817
  function resolveCaseValue2(value, ctx, state) {
6661
6818
  const alias = ctx.currentAlias ?? ctx.rootTable;
6662
- return resolveCaseValue(value, alias, void 0, ctx.naming, state);
6819
+ return resolveCaseValue(
6820
+ value,
6821
+ alias,
6822
+ void 0,
6823
+ ctx.naming,
6824
+ state,
6825
+ void 0,
6826
+ // Handler-level path: renders every expression kind via the shared
6827
+ // expression compiler. It does NOT apply the customFn FILTER modifier
6828
+ // (agg_filter) — that requires compiler-private compileFilterCondition,
6829
+ // which cannot be reached here without a circular dependency. The primary
6830
+ // DX caseWhen path (PlanCompiler.compileCustomExpressionNode) filter-patches
6831
+ // correctly; use it for filtered aggregates as CASE branches.
6832
+ (expr) => compileExpressionIntent(expr, ctx, state)
6833
+ );
6663
6834
  }
6664
6835
  var caseHandler, simpleCaseHandler;
6665
6836
  var init_case = __esm({
@@ -6668,6 +6839,7 @@ var init_case = __esm({
6668
6839
  init_ast_helpers();
6669
6840
  init_param_intent();
6670
6841
  init_case_value();
6842
+ init_custom();
6671
6843
  init_param_value();
6672
6844
  caseHandler = {
6673
6845
  types: ["case", "CASE", "caseWhen"],
@@ -9366,6 +9538,24 @@ function renumberNode(value, offset) {
9366
9538
  }
9367
9539
  return result;
9368
9540
  }
9541
+ function maxParamRefNumber(value) {
9542
+ if (value === null || typeof value !== "object") return 0;
9543
+ if (Array.isArray(value)) {
9544
+ let max2 = 0;
9545
+ for (const item of value) max2 = Math.max(max2, maxParamRefNumber(item));
9546
+ return max2;
9547
+ }
9548
+ const obj = value;
9549
+ if ("ParamRef" in obj && obj.ParamRef !== null && typeof obj.ParamRef === "object") {
9550
+ const n = obj.ParamRef.number;
9551
+ return typeof n === "number" ? n : 0;
9552
+ }
9553
+ let max = 0;
9554
+ for (const key of Object.keys(obj)) {
9555
+ max = Math.max(max, maxParamRefNumber(obj[key]));
9556
+ }
9557
+ return max;
9558
+ }
9369
9559
  var includeHandlersInitialized = false;
9370
9560
  function ensureIncludeHandlersRegistered() {
9371
9561
  if (includeHandlersInitialized) return;
@@ -10400,7 +10590,7 @@ var PlanCompiler = class _PlanCompiler {
10400
10590
  }
10401
10591
  case "selectExpression": {
10402
10592
  if (decision.expressionType === "case") {
10403
- const caseNode = this.compileCaseExpression(decision);
10593
+ const caseNode = this.compileCaseExpression(decision, plan);
10404
10594
  const alias = decision.alias;
10405
10595
  targetList.push({
10406
10596
  ResTarget: {
@@ -10413,34 +10603,7 @@ var PlanCompiler = class _PlanCompiler {
10413
10603
  }
10414
10604
  case "selectCustomExpression": {
10415
10605
  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;
10606
+ const node = this.compileCustomExpressionNode(exprIntent, plan);
10444
10607
  const alias = decision.alias || decision.column || void 0;
10445
10608
  targetList.push({
10446
10609
  ResTarget: {
@@ -10707,14 +10870,28 @@ var PlanCompiler = class _PlanCompiler {
10707
10870
  */
10708
10871
  compileJoinDecision(decision, plan, from) {
10709
10872
  if (isPrecompiledJoinDecision(decision)) {
10710
- if (isBatchValuesJoinDecision(decision)) {
10873
+ const isBatchValues = isBatchValuesJoinDecision(decision);
10874
+ if (isBatchValues) {
10711
10875
  for (const p of decision.batchValuesParams) {
10712
10876
  this.state.parameters.push(p);
10713
10877
  }
10714
10878
  this.state.paramIndex = this.state.parameters.length;
10715
10879
  }
10716
10880
  const jRarg = decision.joinRarg;
10717
- const jOn = decision.joinOnNode;
10881
+ let jOn = decision.joinOnNode;
10882
+ if (!isBatchValues && decision.joinOnParams?.length) {
10883
+ if (maxParamRefNumber(decision.joinOnNode) > decision.joinOnParams.length) {
10884
+ throw new Error(
10885
+ "Table-mode JOIN ON clauses support only inline param() values; externally pre-assigned or bound parameters in an ON clause are not supported."
10886
+ );
10887
+ }
10888
+ const offset = this.state.parameters.length;
10889
+ jOn = renumberParamRefsInAst(decision.joinOnNode, offset);
10890
+ for (const p of decision.joinOnParams) {
10891
+ this.state.parameters.push(p);
10892
+ }
10893
+ this.state.paramIndex = this.state.parameters.length;
10894
+ }
10718
10895
  from[0] = decision.joinType === "left" ? leftJoin(from[0], jRarg, jOn) : innerJoin(from[0], jRarg, jOn);
10719
10896
  } else {
10720
10897
  from[0] = this.compileJoin(decision, plan, from[0]);
@@ -11008,7 +11185,7 @@ var PlanCompiler = class _PlanCompiler {
11008
11185
  // --------------------------------------------------------------------------
11009
11186
  // CASE Expression Compilation
11010
11187
  // --------------------------------------------------------------------------
11011
- compileCaseExpression(decision) {
11188
+ compileCaseExpression(decision, plan) {
11012
11189
  const conditions = decision.conditions;
11013
11190
  const elseValue = decision.value;
11014
11191
  if (!conditions || conditions.length === 0) {
@@ -11016,7 +11193,7 @@ var PlanCompiler = class _PlanCompiler {
11016
11193
  }
11017
11194
  const args = conditions.map((cond) => {
11018
11195
  const whenExpr = this.dispatchWhere(cond.when);
11019
- const thenResult = this.compileCaseValue(cond.then);
11196
+ const thenResult = this.compileCaseValue(cond.then, plan);
11020
11197
  return {
11021
11198
  CaseWhen: {
11022
11199
  expr: whenExpr,
@@ -11026,7 +11203,7 @@ var PlanCompiler = class _PlanCompiler {
11026
11203
  });
11027
11204
  let defresult;
11028
11205
  if (elseValue !== void 0) {
11029
- defresult = this.compileCaseValue(elseValue);
11206
+ defresult = this.compileCaseValue(elseValue, plan);
11030
11207
  }
11031
11208
  return {
11032
11209
  CaseExpr: {
@@ -11039,26 +11216,66 @@ var PlanCompiler = class _PlanCompiler {
11039
11216
  * Compile a CASE THEN/ELSE value based on its ExpressionIntent kind.
11040
11217
  * Delegates to shared resolveCaseValue with nested CASE support.
11041
11218
  */
11042
- compileCaseValue(value) {
11219
+ compileCaseValue(value, plan) {
11043
11220
  return resolveCaseValue(
11044
11221
  value,
11045
11222
  this.currentRootTable,
11046
11223
  void 0,
11047
11224
  this.naming,
11048
11225
  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
- })
11226
+ (expr) => {
11227
+ const nestedDecision = {
11228
+ type: "selectExpression",
11229
+ expressionType: "case",
11230
+ conditions: expr.when.map((wc) => ({
11231
+ when: wc.condition,
11232
+ // biome-ignore lint/suspicious/noThenProperty: intentional
11233
+ then: wc.result
11234
+ })),
11235
+ value: expr.else,
11236
+ table: this.currentRootTable
11237
+ };
11238
+ return this.compileCaseExpression(nestedDecision, plan);
11239
+ },
11240
+ (expr) => this.compileCustomExpressionNode(
11241
+ expr,
11242
+ plan
11243
+ )
11060
11244
  );
11061
11245
  }
11246
+ /**
11247
+ * Compile a custom ExpressionIntent (customFn, customOp, ref, cast, unary,
11248
+ * array, function, subquery, …) to an AST node, applying the customFn FILTER
11249
+ * clause. Shared by the `selectCustomExpression` target path and CASE
11250
+ * THEN/ELSE values so both render the full expression surface identically
11251
+ * (every expression kind + FILTER), rather than one path silently binding
11252
+ * expressions as parameters or dropping FILTER.
11253
+ */
11254
+ compileCustomExpressionNode(exprIntent, plan) {
11255
+ const ctx = this.createHandlerContext(plan, plan.rootTable);
11256
+ const state = this.createHandlerState();
11257
+ const node = compileExpressionIntent(exprIntent, ctx, state);
11258
+ if (exprIntent.kind === "customFn" && exprIntent.filter) {
11259
+ const filterIntent = exprIntent.filter;
11260
+ const filterDecision = convertWhereCondition(
11261
+ filterIntent,
11262
+ plan.rootTable
11263
+ );
11264
+ if (filterDecision) {
11265
+ const filterNode = compileFilterCondition(
11266
+ filterDecision,
11267
+ createWhereDispatcher(),
11268
+ ctx,
11269
+ state
11270
+ );
11271
+ if (filterNode && "FuncCall" in node) {
11272
+ node.FuncCall.agg_filter = filterNode;
11273
+ }
11274
+ }
11275
+ }
11276
+ this.state.paramIndex = state.paramIndex;
11277
+ return node;
11278
+ }
11062
11279
  // --------------------------------------------------------------------------
11063
11280
  // INSERT Compilation
11064
11281
  // --------------------------------------------------------------------------
@@ -11261,6 +11478,9 @@ function compilePlan(plan, options) {
11261
11478
  init_naming_plugin();
11262
11479
  init_validate();
11263
11480
 
11481
+ // src/ddl/phases/comments.ts
11482
+ init_validate();
11483
+
11264
11484
  // src/ddl/phases/types.ts
11265
11485
  function sup(caps, flag) {
11266
11486
  return !caps || flag === true;
@@ -11281,6 +11501,7 @@ var VALID_INDEX_METHODS = /* @__PURE__ */ new Set([
11281
11501
  "bloom"
11282
11502
  ]);
11283
11503
  function validateIndexMethod(method, context = "index method") {
11504
+ assertString(method, `Invalid ${context}`);
11284
11505
  if (!VALID_INDEX_METHODS.has(method)) {
11285
11506
  throw new Error(
11286
11507
  `Invalid ${context}: "${method}". Must be one of: ${[...VALID_INDEX_METHODS].join(", ")}`
@@ -11288,6 +11509,7 @@ function validateIndexMethod(method, context = "index method") {
11288
11509
  }
11289
11510
  }
11290
11511
  function validateEnumLabel(value, context = "enum label") {
11512
+ assertString(value, `Invalid ${context}`);
11291
11513
  if (/\x00/.test(value)) {
11292
11514
  throw new Error(
11293
11515
  `Invalid ${context}: contains NUL byte (\\x00) which would be silently truncated by PostgreSQL`
@@ -11312,6 +11534,7 @@ function quoteCollation(name) {
11312
11534
  return `"${name}"`;
11313
11535
  }
11314
11536
  function quoteRoleName(name) {
11537
+ assertString(name, "quoteRoleName: role name");
11315
11538
  if (!name) {
11316
11539
  throw new Error("quoteRoleName: role name must not be empty");
11317
11540
  }
@@ -11378,17 +11601,21 @@ function generateCommentsPhase(ctx) {
11378
11601
  }
11379
11602
  const statements = [];
11380
11603
  for (const table of tables) {
11381
- if (table.comment) {
11604
+ const tableComment = table.comment;
11605
+ if (tableComment) {
11606
+ assertString(tableComment, "table comment");
11382
11607
  const qualifiedTable = qualifyTableIdent(table.name, schemaName, naming);
11383
11608
  statements.push(
11384
- `COMMENT ON TABLE ${qualifiedTable} IS '${table.comment.replace(/'/g, "''")}';`
11609
+ `COMMENT ON TABLE ${qualifiedTable} IS '${tableComment.replace(/'/g, "''")}';`
11385
11610
  );
11386
11611
  }
11387
11612
  for (const col of table.columns) {
11388
- if (col.comment) {
11613
+ const columnComment = col.comment;
11614
+ if (columnComment) {
11615
+ assertString(columnComment, "column comment");
11389
11616
  const qualifiedTable = qualifyTableIdent(table.name, schemaName, naming);
11390
11617
  statements.push(
11391
- `COMMENT ON COLUMN ${qualifiedTable}.${quoteIdent2(naming.toDatabase(col.name))} IS '${col.comment.replace(/'/g, "''")}';`
11618
+ `COMMENT ON COLUMN ${qualifiedTable}.${quoteIdent2(naming.toDatabase(col.name))} IS '${columnComment.replace(/'/g, "''")}';`
11392
11619
  );
11393
11620
  }
11394
11621
  }
@@ -11413,9 +11640,10 @@ function generateConstraintsPhase(ctx) {
11413
11640
  for (const check of table.checkConstraints ?? []) {
11414
11641
  const qualifiedTable = qualifyTableIdent(table.name, schemaName, naming);
11415
11642
  const constraintName = quoteIdent2(check.name);
11416
- validateSqlExpression(check.expression, "check constraint expression");
11643
+ const expression = check.expression;
11644
+ validateCheckExpression(expression, "check constraint expression");
11417
11645
  statements.push(
11418
- `ALTER TABLE ${qualifiedTable} ADD CONSTRAINT ${constraintName} ${check.expression};`
11646
+ `ALTER TABLE ${qualifiedTable} ADD CONSTRAINT ${constraintName} ${expression};`
11419
11647
  );
11420
11648
  }
11421
11649
  }
@@ -11598,12 +11826,29 @@ function mapOnDeleteAction(action) {
11598
11826
  function qualifyTable(table, schemaName) {
11599
11827
  return schemaName ? `${quoteIdent2(schemaName, "schema")}.${quoteIdent2(table, "table")}` : quoteIdent2(table, "table");
11600
11828
  }
11829
+ function buildDoBlock(body) {
11830
+ const delimiter = chooseDoBlockDelimiter(body);
11831
+ return `DO ${delimiter} ${body} ${delimiter};`;
11832
+ }
11833
+ function chooseDoBlockDelimiter(body) {
11834
+ if (!body.includes("$$")) return "$$";
11835
+ let suffix = 0;
11836
+ let delimiter = "$dbsp_check$";
11837
+ while (body.includes(delimiter)) {
11838
+ suffix++;
11839
+ delimiter = `$dbsp_check_${suffix}$`;
11840
+ }
11841
+ return delimiter;
11842
+ }
11601
11843
  function pkName(table) {
11602
11844
  return `pk_${table}`;
11603
11845
  }
11604
11846
  function fkName(table, columns) {
11605
11847
  return `fk_${table}_${columns.join("_")}`;
11606
11848
  }
11849
+ function uniqueName(table, column) {
11850
+ return `${table}_${column}_key`;
11851
+ }
11607
11852
  function idxName(table, columns, customName) {
11608
11853
  return customName ?? `idx_${table}_${columns.join("_")}`;
11609
11854
  }
@@ -11658,11 +11903,22 @@ function buildPolicySQL(tableName, policy, schemaName) {
11658
11903
  }
11659
11904
  function buildSequenceClause(verb, seqName, seq, includeCycleNoCycle = false) {
11660
11905
  const parts = [`${verb} ${seqName}`];
11661
- if (seq.startWith !== void 0) parts.push(`START WITH ${seq.startWith}`);
11906
+ if (seq.startWith !== void 0)
11907
+ parts.push(
11908
+ `START WITH ${assertNumericLiteral(seq.startWith, "sequence START WITH")}`
11909
+ );
11662
11910
  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}`);
11911
+ parts.push(
11912
+ `INCREMENT BY ${assertNumericLiteral(seq.incrementBy, "sequence INCREMENT BY")}`
11913
+ );
11914
+ if (seq.minValue !== void 0)
11915
+ parts.push(
11916
+ `MINVALUE ${assertNumericLiteral(seq.minValue, "sequence MINVALUE")}`
11917
+ );
11918
+ if (seq.maxValue !== void 0)
11919
+ parts.push(
11920
+ `MAXVALUE ${assertNumericLiteral(seq.maxValue, "sequence MAXVALUE")}`
11921
+ );
11666
11922
  if (includeCycleNoCycle) {
11667
11923
  if (seq.cycle !== void 0) parts.push(seq.cycle ? "CYCLE" : "NO CYCLE");
11668
11924
  } else {
@@ -11672,7 +11928,7 @@ function buildSequenceClause(verb, seqName, seq, includeCycleNoCycle = false) {
11672
11928
  }
11673
11929
  function failSafeUnknownDownChange(kind) {
11674
11930
  return {
11675
- sql: `-- WARNING: Cannot reverse unsupported SchemaChange kind "${kind}"`,
11931
+ sql: `-- WARNING: Cannot reverse unsupported SchemaChange kind "${sanitizeCommentText(kind)}"`,
11676
11932
  destructive: true
11677
11933
  };
11678
11934
  }
@@ -11734,7 +11990,7 @@ function generateMigrationSQL(diff, options) {
11734
11990
  [],
11735
11991
  // 8: alter column
11736
11992
  [],
11737
- // 9: add PK
11993
+ // 9: add PK / column UNIQUE constraint
11738
11994
  [],
11739
11995
  // 10: add FK
11740
11996
  [],
@@ -11821,6 +12077,7 @@ function getPhase(kind) {
11821
12077
  case "alter_column_collation":
11822
12078
  case "alter_column_identity":
11823
12079
  return 8;
12080
+ case "alter_column_unique":
11824
12081
  case "add_primary_key":
11825
12082
  return 9;
11826
12083
  case "add_foreign_key":
@@ -11857,7 +12114,7 @@ function upAddColumn(change, schemaName) {
11857
12114
  }
11858
12115
  function upAlterColumnType(change, schemaName) {
11859
12116
  const col = change.meta?.column;
11860
- const toType = col ? mapColumnType(col) : String(change.meta?.toType);
12117
+ const toType = col ? mapColumnType(col) : validateDbTypeName(change.meta?.toType);
11861
12118
  return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} TYPE ${toType};`;
11862
12119
  }
11863
12120
  function upAlterColumnNullable(change, schemaName) {
@@ -11872,6 +12129,34 @@ function upAlterColumnDefault(change, schemaName) {
11872
12129
  }
11873
12130
  return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} SET DEFAULT ${formatDefault(def)};`;
11874
12131
  }
12132
+ function addColumnUniqueSQL(table, column, schemaName, constraintName) {
12133
+ const name = constraintName ?? uniqueName(table, column);
12134
+ if (constraintName !== void 0) validateIdentifier(constraintName, "alias");
12135
+ const quotedConstraintName = quoteIdent2(name, "alias");
12136
+ return `ALTER TABLE ${qualifyTable(table, schemaName)} ADD CONSTRAINT ${quotedConstraintName} UNIQUE (${quoteIdent2(column, "alias")});`;
12137
+ }
12138
+ function dropColumnUniqueSQL(table, column, schemaName, constraintName) {
12139
+ const name = constraintName ?? uniqueName(table, column);
12140
+ if (constraintName !== void 0) validateIdentifier(constraintName, "alias");
12141
+ const quotedConstraintName = quoteIdent2(name, "alias");
12142
+ return `ALTER TABLE ${qualifyTable(table, schemaName)} DROP CONSTRAINT IF EXISTS ${quotedConstraintName};`;
12143
+ }
12144
+ function upAlterColumnUnique(change, schemaName) {
12145
+ const unique = change.meta?.unique;
12146
+ if (unique === void 0 || !change.column) return void 0;
12147
+ const constraintName = change.meta?.constraintName;
12148
+ return unique ? addColumnUniqueSQL(
12149
+ change.table,
12150
+ change.column,
12151
+ schemaName,
12152
+ constraintName
12153
+ ) : dropColumnUniqueSQL(
12154
+ change.table,
12155
+ change.column,
12156
+ schemaName,
12157
+ constraintName
12158
+ );
12159
+ }
11875
12160
  function upAddPrimaryKey(change, schemaName) {
11876
12161
  const columns = change.meta?.columns;
11877
12162
  const pkCols = columns.map((n) => quoteIdent2(n, "alias")).join(", ");
@@ -11900,8 +12185,9 @@ function upCreateIndex(change, schemaName) {
11900
12185
  "alias"
11901
12186
  );
11902
12187
  const unique = idx.unique ? "UNIQUE " : "";
11903
- if (idx.method) validateIndexMethod(idx.method, "index method");
11904
- const method = idx.method ? ` USING ${idx.method}` : "";
12188
+ const indexMethod = idx.method;
12189
+ if (indexMethod) validateIndexMethod(indexMethod, "index method");
12190
+ const method = indexMethod ? ` USING ${indexMethod}` : "";
11905
12191
  const colParts = [
11906
12192
  ...(idx.expressions ?? []).map((expr) => {
11907
12193
  validateSqlExpression(expr, "index expression");
@@ -11918,10 +12204,11 @@ function upCreateIndex(change, schemaName) {
11918
12204
  const nullsNotDistinct = idx.unique && idx.nullsNotDistinct ? " NULLS NOT DISTINCT" : "";
11919
12205
  const withParams = idx.with && Object.keys(idx.with).length > 0 ? ` WITH (${Object.entries(idx.with).map(([k, v]) => {
11920
12206
  validateIdentifier(k, "alias");
11921
- return `${k} = ${v}`;
12207
+ return `${k} = ${formatStorageParameterValue(v, `index WITH parameter "${k}"`)}`;
11922
12208
  }).join(", ")})` : "";
11923
- if (idx.where) validateSqlExpression(idx.where, "index WHERE predicate");
11924
- const where = idx.where ? ` WHERE ${idx.where}` : "";
12209
+ const whereExpr = idx.where;
12210
+ if (whereExpr) validateSqlExpression(whereExpr, "index WHERE predicate");
12211
+ const where = whereExpr ? ` WHERE ${whereExpr}` : "";
11925
12212
  return `CREATE ${unique}INDEX IF NOT EXISTS ${indexName} ON ${qualifyTable(change.table, schemaName)}${method} (${cols})${include}${nullsNotDistinct}${withParams}${where};`;
11926
12213
  }
11927
12214
  function upDropIndex(change, schemaName) {
@@ -11937,12 +12224,12 @@ function upDropIndex(change, schemaName) {
11937
12224
  function upAddCheckConstraint(change, schemaName) {
11938
12225
  const check = change.meta?.check;
11939
12226
  if (!check) return void 0;
12227
+ const expression = check.expression;
11940
12228
  const notValid = check.notValid ? " NOT VALID" : "";
11941
- validateSqlExpression(
11942
- check.expression,
11943
- "migration check constraint expression"
12229
+ validateCheckExpression(expression, "migration check constraint expression");
12230
+ return buildDoBlock(
12231
+ "BEGIN ALTER TABLE " + qualifyTable(change.table, schemaName) + " ADD CONSTRAINT " + quoteIdent2(check.name, "alias") + " " + expression + notValid + "; EXCEPTION WHEN duplicate_object THEN NULL; END"
11944
12232
  );
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
12233
  }
11947
12234
  function upValidateConstraint(change, schemaName) {
11948
12235
  const fk = change.meta?.fk;
@@ -12011,6 +12298,7 @@ function upAlterColumnIdentity(change, schemaName) {
12011
12298
  function upAddComment(change, schemaName) {
12012
12299
  const comment = change.meta?.comment;
12013
12300
  const target = change.meta?.target;
12301
+ assertString(comment, "migration comment");
12014
12302
  const escaped = comment.replace(/'/g, "''");
12015
12303
  if (target === "table") {
12016
12304
  return `COMMENT ON TABLE ${qualifyTable(change.table, schemaName)} IS '${escaped}';`;
@@ -12038,6 +12326,8 @@ function changeToUpSQL(change, schemaName) {
12038
12326
  return upAlterColumnNullable(change, schemaName);
12039
12327
  case "alter_column_default":
12040
12328
  return upAlterColumnDefault(change, schemaName);
12329
+ case "alter_column_unique":
12330
+ return upAlterColumnUnique(change, schemaName);
12041
12331
  case "add_primary_key":
12042
12332
  return upAddPrimaryKey(change, schemaName);
12043
12333
  case "drop_primary_key":
@@ -12135,7 +12425,7 @@ function changeToDownSQL(change, schemaName) {
12135
12425
  };
12136
12426
  case "drop_table":
12137
12427
  return {
12138
- sql: `-- WARNING: Cannot reverse drop_table "${change.table}" -- table data was lost`,
12428
+ sql: `-- WARNING: Cannot reverse drop_table "${sanitizeCommentText(change.table)}" -- table data was lost`,
12139
12429
  destructive: true
12140
12430
  };
12141
12431
  case "add_column":
@@ -12145,19 +12435,20 @@ function changeToDownSQL(change, schemaName) {
12145
12435
  };
12146
12436
  case "drop_column":
12147
12437
  return {
12148
- sql: `-- WARNING: Cannot reverse drop_column "${change.table}"."${change.column}" -- column data was lost`,
12438
+ sql: `-- WARNING: Cannot reverse drop_column "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- column data was lost`,
12149
12439
  destructive: true
12150
12440
  };
12151
12441
  case "alter_column_type": {
12152
12442
  const fromType = change.meta?.fromType;
12153
- if (!fromType) {
12443
+ if (fromType == null) {
12154
12444
  return {
12155
- sql: `-- WARNING: Cannot reverse alter_column_type "${change.table}"."${change.column}" -- missing migration metadata`,
12445
+ sql: `-- WARNING: Cannot reverse alter_column_type "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- missing migration metadata`,
12156
12446
  destructive: true
12157
12447
  };
12158
12448
  }
12449
+ const safeFromType = validateDbTypeName(fromType);
12159
12450
  return {
12160
- sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} TYPE ${fromType};`,
12451
+ sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} TYPE ${safeFromType};`,
12161
12452
  destructive: true
12162
12453
  };
12163
12454
  }
@@ -12165,7 +12456,7 @@ function changeToDownSQL(change, schemaName) {
12165
12456
  const oldNullable = change.meta?.oldNullable;
12166
12457
  if (oldNullable === void 0) {
12167
12458
  return {
12168
- sql: `-- WARNING: Cannot reverse alter_column_nullable "${change.table}"."${change.column}" -- missing migration metadata`,
12459
+ sql: `-- WARNING: Cannot reverse alter_column_nullable "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- missing migration metadata`,
12169
12460
  destructive: true
12170
12461
  };
12171
12462
  }
@@ -12180,7 +12471,7 @@ function changeToDownSQL(change, schemaName) {
12180
12471
  const oldDefault = change.meta?.oldDefault;
12181
12472
  if (oldDefault === void 0) {
12182
12473
  return {
12183
- sql: `-- WARNING: Cannot reverse alter_column_default "${change.table}"."${change.column}" -- missing migration metadata`,
12474
+ sql: `-- WARNING: Cannot reverse alter_column_default "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- missing migration metadata`,
12184
12475
  destructive: true
12185
12476
  };
12186
12477
  }
@@ -12197,6 +12488,30 @@ function changeToDownSQL(change, schemaName) {
12197
12488
  destructive: false
12198
12489
  };
12199
12490
  }
12491
+ case "alter_column_unique": {
12492
+ const unique = change.meta?.unique;
12493
+ if (unique === void 0 || !change.column) {
12494
+ return {
12495
+ sql: `-- WARNING: Cannot reverse alter_column_unique "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- missing migration metadata`,
12496
+ destructive: true
12497
+ };
12498
+ }
12499
+ const constraintName = change.meta?.constraintName;
12500
+ return {
12501
+ sql: unique ? dropColumnUniqueSQL(
12502
+ change.table,
12503
+ change.column,
12504
+ schemaName,
12505
+ constraintName
12506
+ ) : addColumnUniqueSQL(
12507
+ change.table,
12508
+ change.column,
12509
+ schemaName,
12510
+ constraintName
12511
+ ),
12512
+ destructive: unique === true
12513
+ };
12514
+ }
12200
12515
  case "add_primary_key": {
12201
12516
  return {
12202
12517
  sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${quoteIdent2(pkName(change.table), "alias")} CASCADE;`,
@@ -12214,7 +12529,7 @@ function changeToDownSQL(change, schemaName) {
12214
12529
  };
12215
12530
  }
12216
12531
  return {
12217
- sql: `-- WARNING: Cannot reverse drop_primary_key "${change.table}" -- columns unknown`,
12532
+ sql: `-- WARNING: Cannot reverse drop_primary_key "${sanitizeCommentText(change.table)}" -- columns unknown`,
12218
12533
  destructive: true
12219
12534
  };
12220
12535
  }
@@ -12234,7 +12549,7 @@ function changeToDownSQL(change, schemaName) {
12234
12549
  const fk = change.meta?.fk;
12235
12550
  if (!fk) {
12236
12551
  return {
12237
- sql: `-- WARNING: Cannot reverse drop_foreign_key "${change.table}" -- FK definition was lost`,
12552
+ sql: `-- WARNING: Cannot reverse drop_foreign_key "${sanitizeCommentText(change.table)}" -- FK definition was lost`,
12238
12553
  destructive: true
12239
12554
  };
12240
12555
  }
@@ -12249,7 +12564,7 @@ function changeToDownSQL(change, schemaName) {
12249
12564
  const fk = change.meta?.fk;
12250
12565
  if (!oldFk || !fk) {
12251
12566
  return {
12252
- sql: `-- WARNING: Cannot reverse alter_foreign_key "${change.table}" -- missing migration metadata`,
12567
+ sql: `-- WARNING: Cannot reverse alter_foreign_key "${sanitizeCommentText(change.table)}" -- missing migration metadata`,
12253
12568
  destructive: true
12254
12569
  };
12255
12570
  }
@@ -12279,7 +12594,7 @@ ${add}`, destructive: false };
12279
12594
  const idx = change.meta?.index;
12280
12595
  if (!idx) {
12281
12596
  return {
12282
- sql: `-- WARNING: Cannot reverse drop_index "${change.table}" -- index definition was lost`,
12597
+ sql: `-- WARNING: Cannot reverse drop_index "${sanitizeCommentText(change.table)}" -- index definition was lost`,
12283
12598
  destructive: true
12284
12599
  };
12285
12600
  }
@@ -12300,12 +12615,12 @@ ${add}`, destructive: false };
12300
12615
  case "drop_check_constraint": {
12301
12616
  const check = change.meta?.check;
12302
12617
  if (!check) return { sql: void 0, destructive: true };
12303
- validateSqlExpression(
12304
- check.expression,
12305
- "migration check constraint (down)"
12306
- );
12618
+ const expression = check.expression;
12619
+ validateCheckExpression(expression, "migration check constraint (down)");
12307
12620
  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 $$;",
12621
+ sql: buildDoBlock(
12622
+ "BEGIN ALTER TABLE " + qualifyTable(change.table, schemaName) + " ADD CONSTRAINT " + quoteIdent2(check.name, "alias") + " " + expression + "; EXCEPTION WHEN duplicate_object THEN NULL; END"
12623
+ ),
12309
12624
  // Allowlisted: re-adds the dropped CHECK constraint from metadata.
12310
12625
  destructive: false
12311
12626
  };
@@ -12323,7 +12638,10 @@ ${add}`, destructive: false };
12323
12638
  const enumDef = change.meta?.enum;
12324
12639
  if (!enumDef) return { sql: void 0, destructive: true };
12325
12640
  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(", ");
12641
+ const values = enumDef.values.map((v) => {
12642
+ validateEnumLabel(v, "enum value");
12643
+ return `'${v.replace(/'/g, "''")}'`;
12644
+ }).join(", ");
12327
12645
  return {
12328
12646
  sql: `CREATE TYPE ${enumName} AS ENUM (${values});`,
12329
12647
  // Allowlisted: re-creates the dropped enum type from metadata.
@@ -12337,7 +12655,7 @@ ${add}`, destructive: false };
12337
12655
  };
12338
12656
  case "alter_column_collation": {
12339
12657
  return {
12340
- sql: `-- WARNING: Cannot reverse alter_column_collation "${change.table}"."${change.column}" -- previous collation unknown`,
12658
+ sql: `-- WARNING: Cannot reverse alter_column_collation "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- previous collation unknown`,
12341
12659
  destructive: true
12342
12660
  };
12343
12661
  }
@@ -12363,7 +12681,7 @@ ${add}`, destructive: false };
12363
12681
  }
12364
12682
  if (!prevIdentity && !col.identity) {
12365
12683
  return {
12366
- sql: `-- WARNING: Cannot reverse alter_column_identity "${change.table}"."${change.column}" -- missing previous identity metadata`,
12684
+ sql: `-- WARNING: Cannot reverse alter_column_identity "${sanitizeCommentText(change.table)}"."${sanitizeCommentText(change.column)}" -- missing previous identity metadata`,
12367
12685
  destructive: true
12368
12686
  };
12369
12687
  }
@@ -12390,7 +12708,8 @@ ${add}`, destructive: false };
12390
12708
  case "drop_comment": {
12391
12709
  const target = change.meta?.target;
12392
12710
  const comment = change.meta?.comment;
12393
- if (typeof comment === "string") {
12711
+ if (comment !== void 0) {
12712
+ assertString(comment, "migration comment");
12394
12713
  const escaped = comment.replace(/'/g, "''");
12395
12714
  if (target === "table") {
12396
12715
  return {
@@ -12407,8 +12726,9 @@ ${add}`, destructive: false };
12407
12726
  };
12408
12727
  }
12409
12728
  }
12729
+ const columnTarget2 = change.column === void 0 ? "" : `."${sanitizeCommentText(change.column)}"`;
12410
12730
  return {
12411
- sql: `-- WARNING: Cannot reverse drop_comment "${change.table}"${change.column ? `."${change.column}"` : ""} -- comment text was lost`,
12731
+ sql: '-- WARNING: Cannot reverse drop_comment "' + sanitizeCommentText(change.table) + '"' + columnTarget2 + " -- comment text was lost",
12412
12732
  destructive: true
12413
12733
  };
12414
12734
  }
@@ -12442,7 +12762,7 @@ ${add}`, destructive: false };
12442
12762
  const prevSeq = change.meta?.previousSequence;
12443
12763
  if (!prevSeq) {
12444
12764
  return {
12445
- sql: `-- WARNING: Cannot reverse alter_sequence "${change.table}" -- missing migration metadata`,
12765
+ sql: `-- WARNING: Cannot reverse alter_sequence "${sanitizeCommentText(change.table)}" -- missing migration metadata`,
12446
12766
  destructive: true
12447
12767
  };
12448
12768
  }
@@ -12465,7 +12785,7 @@ ${add}`, destructive: false };
12465
12785
  }
12466
12786
  case "validate_constraint":
12467
12787
  return {
12468
- sql: `-- VALIDATE CONSTRAINT cannot be reversed in PostgreSQL (table: "${change.table}")`,
12788
+ sql: `-- VALIDATE CONSTRAINT cannot be reversed in PostgreSQL (table: "${sanitizeCommentText(change.table)}")`,
12469
12789
  destructive: true
12470
12790
  };
12471
12791
  case "enable_rls":
@@ -12506,7 +12826,6 @@ function generateDownSQL(diff, options) {
12506
12826
  function generateDownMigrationSQL(diff, options) {
12507
12827
  const schemaName = options?.schemaName;
12508
12828
  const includeDestructive = options?.includeDestructive ?? true;
12509
- const changes = includeDestructive ? diff.changes : diff.changes.filter((c) => !c.destructive);
12510
12829
  const phases = [
12511
12830
  [],
12512
12831
  // 0: drop FK, drop CHECK
@@ -12527,7 +12846,7 @@ function generateDownMigrationSQL(diff, options) {
12527
12846
  [],
12528
12847
  // 8: alter column
12529
12848
  [],
12530
- // 9: add PK
12849
+ // 9: add PK / column UNIQUE constraint
12531
12850
  [],
12532
12851
  // 10: add FK
12533
12852
  [],
@@ -12547,7 +12866,7 @@ function generateDownMigrationSQL(diff, options) {
12547
12866
  []
12548
12867
  // 18: CREATE/DROP POLICY
12549
12868
  ];
12550
- for (const change of changes) {
12869
+ for (const change of diff.changes) {
12551
12870
  const phase = getPhase(change.kind);
12552
12871
  phases[phase].push(change);
12553
12872
  }
@@ -12556,6 +12875,7 @@ function generateDownMigrationSQL(diff, options) {
12556
12875
  for (let i = phases.length - 1; i >= 0; i--) {
12557
12876
  for (const change of phases[i]) {
12558
12877
  const down = changeToDownSQL(change, schemaName);
12878
+ if (!includeDestructive && down.destructive) continue;
12559
12879
  destructive = down.destructive || destructive;
12560
12880
  if (down.sql) statements.push(down.sql);
12561
12881
  }
@@ -12600,7 +12920,10 @@ function generateAddFKSQL(tableName, fk, schemaName) {
12600
12920
  const qualTable = qualifyTable(tableName, schemaName);
12601
12921
  const constraintName = quoteIdent2(fkName(tableName, fk.columns), "alias");
12602
12922
  const fkCols = fk.columns.map((n) => quoteIdent2(n, "alias")).join(", ");
12603
- const refTable = qualifyTable(fk.references.table, schemaName);
12923
+ const refTable = qualifyTable(
12924
+ fk.references.table,
12925
+ fk.references.schema ?? schemaName
12926
+ );
12604
12927
  const refCols = fk.references.columns.map((n) => quoteIdent2(n, "alias")).join(", ");
12605
12928
  const onDelete = fk.onDelete ? ` ON DELETE ${mapOnDeleteAction(fk.onDelete)}` : "";
12606
12929
  const onUpdate = fk.onUpdate ? ` ON UPDATE ${mapOnDeleteAction(fk.onUpdate)}` : "";
@@ -12686,10 +13009,11 @@ function quoteIdentifier(name) {
12686
13009
  validateIdentifier(name, "alias");
12687
13010
  return `"${name.replace(/"/g, '""')}"`;
12688
13011
  }
12689
- function qualifyTable2(tableName, schemaName, naming) {
13012
+ function qualifyTable2(tableName, schemaName, naming, transformSchema = true) {
12690
13013
  const table = quoteIdentifier(naming.toDatabase(tableName));
12691
13014
  if (schemaName) {
12692
- return `${quoteIdentifier(naming.toDatabase(schemaName))}.${table}`;
13015
+ const schema = transformSchema ? naming.toDatabase(schemaName) : schemaName;
13016
+ return `${quoteIdentifier(schema)}.${table}`;
12693
13017
  }
12694
13018
  return table;
12695
13019
  }
@@ -12760,7 +13084,7 @@ function generateAlterTableAddFK(tableName, fk, schemaName, naming) {
12760
13084
  `fk_${tableName}_${fk.columns.join("_")}`
12761
13085
  );
12762
13086
  const fkCols = fk.columns.map((col) => quoteIdentifier(naming.toDatabase(col))).join(", ");
12763
- const refTable = qualifyTable2(fk.references.table, schemaName, naming);
13087
+ const refTable = fk.references.schema !== void 0 ? qualifyTable2(fk.references.table, fk.references.schema, naming, false) : qualifyTable2(fk.references.table, schemaName, naming);
12764
13088
  const refCols = fk.references.columns.map((col) => quoteIdentifier(naming.toDatabase(col))).join(", ");
12765
13089
  const onDelete = fk.onDelete ? ` ON DELETE ${mapOnDeleteAction(fk.onDelete)}` : "";
12766
13090
  const onUpdate = fk.onUpdate ? ` ON UPDATE ${mapOnDeleteAction(fk.onUpdate)}` : "";
@@ -12774,8 +13098,9 @@ function generateCreateIndex(tableName, idx, schemaName, naming) {
12774
13098
  );
12775
13099
  const qualifiedTable = qualifyTable2(tableName, schemaName, naming);
12776
13100
  const unique = idx.unique ? "UNIQUE " : "";
12777
- if (idx.method) validateIndexMethod(idx.method, "index method");
12778
- const method = idx.method ? ` USING ${idx.method}` : "";
13101
+ const indexMethod = idx.method;
13102
+ if (indexMethod) validateIndexMethod(indexMethod, "index method");
13103
+ const method = indexMethod ? ` USING ${indexMethod}` : "";
12779
13104
  const colParts = [];
12780
13105
  if (idx.expressions && idx.expressions.length > 0) {
12781
13106
  for (const expr of idx.expressions) {
@@ -12795,10 +13120,11 @@ function generateCreateIndex(tableName, idx, schemaName, naming) {
12795
13120
  const nullsNotDistinct = idx.unique && idx.nullsNotDistinct ? " NULLS NOT DISTINCT" : "";
12796
13121
  const withParams = idx.with && Object.keys(idx.with).length > 0 ? ` WITH (${Object.entries(idx.with).map(([k, v]) => {
12797
13122
  validateIdentifier(k, "alias");
12798
- return `${k} = ${v}`;
13123
+ return `${k} = ${formatStorageParameterValue(v, `index WITH parameter "${k}"`)}`;
12799
13124
  }).join(", ")})` : "";
12800
- if (idx.where) validateSqlExpression(idx.where, "index WHERE predicate");
12801
- const where = idx.where ? ` WHERE ${idx.where}` : "";
13125
+ const whereExpr = idx.where;
13126
+ if (whereExpr) validateSqlExpression(whereExpr, "index WHERE predicate");
13127
+ const where = whereExpr ? ` WHERE ${whereExpr}` : "";
12802
13128
  return `CREATE ${unique}INDEX ${indexName} ON ${qualifiedTable}${method} (${cols})${include}${nullsNotDistinct}${withParams}${where};`;
12803
13129
  }
12804
13130
  function generateCreatePolicy(tableName, policy, schemaName, naming) {
@@ -13165,7 +13491,8 @@ function normalizeTable(table, plugin) {
13165
13491
  columns: fk.columns.map(toDb),
13166
13492
  references: {
13167
13493
  table: toDb(fk.references.table),
13168
- columns: fk.references.columns.map(toDb)
13494
+ columns: fk.references.columns.map(toDb),
13495
+ ...fk.references.schema !== void 0 ? { schema: fk.references.schema } : {}
13169
13496
  }
13170
13497
  })),
13171
13498
  indexes: table.indexes.map((idx) => ({
@@ -13258,6 +13585,21 @@ function compareColumnDetails(tableName, schema, db, changes) {
13258
13585
  meta: { default: schema.default, oldDefault: db.default }
13259
13586
  });
13260
13587
  }
13588
+ const schemaUnique = schema.unique === true;
13589
+ const dbUnique = db.unique === true;
13590
+ if (schemaUnique !== dbUnique) {
13591
+ changes.push({
13592
+ kind: "alter_column_unique",
13593
+ table: tableName,
13594
+ column: schema.name,
13595
+ destructive: schemaUnique === false,
13596
+ details: `Change unique of "${schema.name}" from ${dbUnique} to ${schemaUnique}`,
13597
+ meta: {
13598
+ unique: schemaUnique,
13599
+ ...db.uniqueConstraintName !== void 0 ? { constraintName: db.uniqueConstraintName } : {}
13600
+ }
13601
+ });
13602
+ }
13261
13603
  if ((schema.collation ?? null) !== (db.collation ?? null)) {
13262
13604
  changes.push({
13263
13605
  kind: "alter_column_collation",
@@ -13329,7 +13671,8 @@ function comparePrimaryKeys(schema, db, changes) {
13329
13671
  kind: "drop_primary_key",
13330
13672
  table: schema.name,
13331
13673
  destructive: true,
13332
- details: `Drop primary key (${dbPK.join(", ")})`
13674
+ details: `Drop primary key (${dbPK.join(", ")})`,
13675
+ meta: { columns: dbPK }
13333
13676
  });
13334
13677
  return;
13335
13678
  }
@@ -13337,7 +13680,8 @@ function comparePrimaryKeys(schema, db, changes) {
13337
13680
  kind: "drop_primary_key",
13338
13681
  table: schema.name,
13339
13682
  destructive: true,
13340
- details: `Drop primary key (${dbPK.join(", ")})`
13683
+ details: `Drop primary key (${dbPK.join(", ")})`,
13684
+ meta: { columns: dbPK }
13341
13685
  });
13342
13686
  changes.push({
13343
13687
  kind: "add_primary_key",
@@ -13409,7 +13753,7 @@ function compareForeignKeys(schema, db, changes) {
13409
13753
  }
13410
13754
  }
13411
13755
  function fkKey(fk) {
13412
- return `${fk.columns.join(",")}\u2192${fk.references.table}(${fk.references.columns.join(",")})`;
13756
+ return `${fk.columns.join(",")}\u2192${fk.references.schema ?? ""}.${fk.references.table}(${fk.references.columns.join(",")})`;
13413
13757
  }
13414
13758
  function compareIndexes(schema, db, changes) {
13415
13759
  const explicitIndexCols = new Set(
@@ -13442,16 +13786,15 @@ function compareIndexes(schema, db, changes) {
13442
13786
  });
13443
13787
  }
13444
13788
  }
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
- )
13789
+ const dbColMap = new Map(db.columns.map((col) => [col.name, col]));
13790
+ const autoUniqueIndexColumns = new Set(
13791
+ schema.columns.filter((schemaCol) => {
13792
+ const dbCol = dbColMap.get(schemaCol.name);
13793
+ return dbCol !== void 0 && (schemaCol.unique === true || dbCol.unique === true);
13794
+ }).map((col) => col.name)
13452
13795
  );
13453
13796
  for (const [key, idx] of dbIdxMap) {
13454
- if (!schemaIdxMap.has(key) && !autoFkIndexKeys.has(key) && !autoUniqueIndexKeys.has(key)) {
13797
+ if (!schemaIdxMap.has(key) && !autoFkIndexKeys.has(key) && !isAutoUniqueIndex(schema.name, idx, autoUniqueIndexColumns)) {
13455
13798
  changes.push({
13456
13799
  kind: "drop_index",
13457
13800
  table: schema.name,
@@ -13462,6 +13805,16 @@ function compareIndexes(schema, db, changes) {
13462
13805
  }
13463
13806
  }
13464
13807
  }
13808
+ function isAutoUniqueIndex(tableName, idx, autoUniqueIndexColumns) {
13809
+ if (idx.columns.length !== 1) return false;
13810
+ const column = idx.columns[0];
13811
+ if (column === void 0 || !autoUniqueIndexColumns.has(column)) return false;
13812
+ if (idx.unique !== true) return false;
13813
+ if (idx.name !== void 0 && idx.name !== `${tableName}_${column}_key`) {
13814
+ return false;
13815
+ }
13816
+ 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);
13817
+ }
13465
13818
  function indexKey(idx) {
13466
13819
  const parts = [
13467
13820
  idx.columns.join(","),
@@ -13826,6 +14179,7 @@ function buildSummary(changes) {
13826
14179
  case "alter_column_type":
13827
14180
  case "alter_column_nullable":
13828
14181
  case "alter_column_default":
14182
+ case "alter_column_unique":
13829
14183
  columns.altered++;
13830
14184
  break;
13831
14185
  case "add_primary_key":
@@ -14076,6 +14430,7 @@ async function queryAllCatalogs(pool, schema) {
14076
14430
  pksResult,
14077
14431
  fksResult,
14078
14432
  indexesResult,
14433
+ uniqueColumnsResult,
14079
14434
  enumsResult,
14080
14435
  commentsResult,
14081
14436
  checksResult,
@@ -14109,28 +14464,44 @@ async function queryAllCatalogs(pool, schema) {
14109
14464
  // 3. Foreign keys
14110
14465
  pool.query(
14111
14466
  `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`,
14467
+ c.conname AS constraint_name,
14468
+ source_rel.relname AS source_table,
14469
+ source_attr.attname AS source_column,
14470
+ target_ns.nspname AS target_schema,
14471
+ target_rel.relname AS target_table,
14472
+ target_attr.attname AS target_column,
14473
+ CASE c.confdeltype
14474
+ WHEN 'c' THEN 'CASCADE'
14475
+ WHEN 'n' THEN 'SET NULL'
14476
+ WHEN 'd' THEN 'SET DEFAULT'
14477
+ WHEN 'r' THEN 'RESTRICT'
14478
+ ELSE 'NO ACTION'
14479
+ END AS delete_rule,
14480
+ CASE c.confupdtype
14481
+ WHEN 'c' THEN 'CASCADE'
14482
+ WHEN 'n' THEN 'SET NULL'
14483
+ WHEN 'd' THEN 'SET DEFAULT'
14484
+ WHEN 'r' THEN 'RESTRICT'
14485
+ ELSE 'NO ACTION'
14486
+ END AS update_rule,
14487
+ CASE WHEN c.condeferrable THEN 'YES' ELSE 'NO' END AS is_deferrable,
14488
+ CASE WHEN c.condeferred THEN 'YES' ELSE 'NO' END AS initially_deferred
14489
+ FROM pg_constraint c
14490
+ JOIN pg_class source_rel ON source_rel.oid = c.conrelid
14491
+ JOIN pg_namespace source_ns ON source_ns.oid = source_rel.relnamespace
14492
+ JOIN pg_class target_rel ON target_rel.oid = c.confrelid
14493
+ JOIN pg_namespace target_ns ON target_ns.oid = target_rel.relnamespace
14494
+ JOIN LATERAL unnest(c.conkey, c.confkey) WITH ORDINALITY AS cols(source_attnum, target_attnum, ord) ON true
14495
+ JOIN pg_attribute source_attr
14496
+ ON source_attr.attrelid = c.conrelid
14497
+ AND source_attr.attnum = cols.source_attnum
14498
+ JOIN pg_attribute target_attr
14499
+ ON target_attr.attrelid = c.confrelid
14500
+ AND target_attr.attnum = cols.target_attnum
14501
+ WHERE c.contype = 'f'
14502
+ AND c.conparentid = 0
14503
+ AND source_ns.nspname = $1
14504
+ ORDER BY c.conname, cols.ord`,
14134
14505
  [schema]
14135
14506
  ),
14136
14507
  // 4. Indexes (excluding PK-backing indexes and unique-constraint-backing indexes).
@@ -14194,7 +14565,19 @@ async function queryAllCatalogs(pool, schema) {
14194
14565
  ORDER BY t.relname, i.relname`,
14195
14566
  [schema]
14196
14567
  ),
14197
- // 5. ENUM types
14568
+ // 5. Single-column UNIQUE constraints tracked as ColumnIR.unique
14569
+ pool.query(
14570
+ `SELECT rel.relname AS table_name, att.attname AS column_name, c.conname AS constraint_name
14571
+ FROM pg_constraint c
14572
+ JOIN pg_class rel ON rel.oid = c.conrelid
14573
+ JOIN pg_namespace ns ON ns.oid = rel.relnamespace
14574
+ JOIN pg_attribute att ON att.attrelid = c.conrelid AND att.attnum = c.conkey[1]
14575
+ WHERE c.contype = 'u'
14576
+ AND array_length(c.conkey, 1) = 1
14577
+ AND ns.nspname = $1`,
14578
+ [schema]
14579
+ ),
14580
+ // 6. ENUM types
14198
14581
  pool.query(
14199
14582
  `SELECT
14200
14583
  t.typname AS name,
@@ -14207,7 +14590,7 @@ async function queryAllCatalogs(pool, schema) {
14207
14590
  GROUP BY t.typname`,
14208
14591
  [schema]
14209
14592
  ),
14210
- // 6. Comments (table and column level) from pg_description
14593
+ // 7. Comments (table and column level) from pg_description
14211
14594
  pool.query(
14212
14595
  `SELECT
14213
14596
  c.relname AS table_name,
@@ -14221,7 +14604,7 @@ async function queryAllCatalogs(pool, schema) {
14221
14604
  AND d.objsubid >= 0`,
14222
14605
  [schema]
14223
14606
  ),
14224
- // 7. CHECK constraints
14607
+ // 8. CHECK constraints
14225
14608
  pool.query(
14226
14609
  `SELECT
14227
14610
  c.conname AS name,
@@ -14233,7 +14616,7 @@ async function queryAllCatalogs(pool, schema) {
14233
14616
  AND n.nspname = $1`,
14234
14617
  [schema]
14235
14618
  ),
14236
- // 8. Partition configurations
14619
+ // 9. Partition configurations
14237
14620
  pool.query(
14238
14621
  `SELECT
14239
14622
  c.relname AS table_name,
@@ -14248,13 +14631,13 @@ async function queryAllCatalogs(pool, schema) {
14248
14631
  GROUP BY c.relname, p.partstrat`,
14249
14632
  [schema]
14250
14633
  ),
14251
- // 9. Installed extensions (no schema param — queries globally; skip plpgsql)
14634
+ // 10. Installed extensions (no schema param — queries globally; skip plpgsql)
14252
14635
  pool.query(
14253
14636
  `SELECT extname AS name
14254
14637
  FROM pg_extension
14255
14638
  WHERE extname != 'plpgsql'`
14256
14639
  ),
14257
- // 10. Sequences not backed by SERIAL
14640
+ // 11. Sequences not backed by SERIAL
14258
14641
  pool.query(
14259
14642
  `SELECT s.sequencename AS name, s.start_value, s.increment_by, s.min_value, s.max_value, s.cycle
14260
14643
  FROM pg_sequences s
@@ -14265,7 +14648,7 @@ async function queryAllCatalogs(pool, schema) {
14265
14648
  AND d.objid IS NULL`,
14266
14649
  [schema]
14267
14650
  ),
14268
- // 11. RLS enabled state per table
14651
+ // 12. RLS enabled state per table
14269
14652
  pool.query(
14270
14653
  `SELECT c.relname AS table_name, c.relrowsecurity AS rls_enabled
14271
14654
  FROM pg_class c
@@ -14274,7 +14657,7 @@ async function queryAllCatalogs(pool, schema) {
14274
14657
  AND c.relkind = 'r'`,
14275
14658
  [schema]
14276
14659
  ),
14277
- // 12. Row-Level Security policies
14660
+ // 13. Row-Level Security policies
14278
14661
  pool.query(
14279
14662
  `SELECT
14280
14663
  c.relname AS table_name,
@@ -14296,6 +14679,7 @@ async function queryAllCatalogs(pool, schema) {
14296
14679
  pks: pksResult.rows,
14297
14680
  fks: fksResult.rows,
14298
14681
  indexes: indexesResult.rows,
14682
+ uniqueColumns: uniqueColumnsResult.rows,
14299
14683
  enums: enumsResult.rows,
14300
14684
  comments: commentsResult.rows,
14301
14685
  checks: checksResult.rows,
@@ -14381,6 +14765,21 @@ function buildIndexMap(rows) {
14381
14765
  }
14382
14766
  return result;
14383
14767
  }
14768
+ function buildUniqueColumnMap(rows) {
14769
+ const result = /* @__PURE__ */ new Map();
14770
+ for (const row of rows) {
14771
+ const existing = result.get(row.table_name);
14772
+ if (existing) {
14773
+ existing.set(row.column_name, row.constraint_name);
14774
+ } else {
14775
+ result.set(
14776
+ row.table_name,
14777
+ /* @__PURE__ */ new Map([[row.column_name, row.constraint_name]])
14778
+ );
14779
+ }
14780
+ }
14781
+ return result;
14782
+ }
14384
14783
  function buildColumnMap(rows) {
14385
14784
  const result = /* @__PURE__ */ new Map();
14386
14785
  for (const col of rows) {
@@ -14408,14 +14807,16 @@ function buildPKMap(rows) {
14408
14807
  function buildFKMap(rows) {
14409
14808
  const result = /* @__PURE__ */ new Map();
14410
14809
  for (const fk of rows) {
14411
- const existing = result.get(fk.constraint_name);
14810
+ const key = JSON.stringify([fk.source_table, fk.constraint_name]);
14811
+ const existing = result.get(key);
14412
14812
  if (existing) {
14413
14813
  existing.cols.push(fk.source_column);
14414
14814
  existing.refs.push(fk.target_column);
14415
14815
  } else {
14416
- result.set(fk.constraint_name, {
14816
+ result.set(key, {
14417
14817
  source: fk.source_table,
14418
14818
  target: fk.target_table,
14819
+ targetSchema: fk.target_schema,
14419
14820
  cols: [fk.source_column],
14420
14821
  refs: [fk.target_column],
14421
14822
  deleteRule: fk.delete_rule,
@@ -14481,10 +14882,12 @@ function buildRLSMaps(rlsRows, policiesRows) {
14481
14882
  function buildTableIR(tableName, ctx) {
14482
14883
  const rawCols = ctx.tableColumns.get(tableName) ?? [];
14483
14884
  const pkCols = ctx.tablePKs.get(tableName);
14885
+ const uniqueColumns = ctx.uniqueColumns.get(tableName);
14484
14886
  const columns = rawCols.map((col) => {
14485
14887
  const identity = col.is_identity === "YES" && col.identity_generation ? col.identity_generation === "ALWAYS" ? "always" : "byDefault" : void 0;
14486
14888
  const collation = col.collation_name && col.collation_name !== "default" ? col.collation_name : void 0;
14487
14889
  const colComment = ctx.columnComments.get(`${tableName}.${col.column_name}`) ?? void 0;
14890
+ const uniqueConstraintName = uniqueColumns?.get(col.column_name);
14488
14891
  return {
14489
14892
  name: col.column_name,
14490
14893
  type: mapPgType(col.data_type, col.udt_name),
@@ -14494,6 +14897,7 @@ function buildTableIR(tableName, ctx) {
14494
14897
  // of quoting it as a string literal.
14495
14898
  ...col.column_default != null ? { default: { sql: col.column_default } } : {},
14496
14899
  originalDbType: col.udt_name,
14900
+ ...uniqueConstraintName !== void 0 ? { unique: true, uniqueConstraintName } : {},
14497
14901
  ...collation ? { collation } : {},
14498
14902
  ...identity ? { identity } : {},
14499
14903
  ...colComment ? { comment: colComment } : {}
@@ -14502,12 +14906,17 @@ function buildTableIR(tableName, ctx) {
14502
14906
  const foreignKeys = [];
14503
14907
  for (const [, fk] of ctx.fksByConstraint) {
14504
14908
  if (fk.source !== tableName) continue;
14505
- if (!ctx.tableNames.includes(fk.target)) continue;
14909
+ const isCrossSchema = fk.targetSchema !== void 0 && fk.targetSchema !== ctx.schema;
14910
+ if (!isCrossSchema && !ctx.tableNames.includes(fk.target)) continue;
14506
14911
  const onDelete = mapDeleteRule(fk.deleteRule);
14507
14912
  const onUpdate = mapDeleteRule(fk.updateRule);
14508
14913
  foreignKeys.push({
14509
14914
  columns: fk.cols,
14510
- references: { table: fk.target, columns: fk.refs },
14915
+ references: {
14916
+ table: fk.target,
14917
+ columns: fk.refs,
14918
+ ...isCrossSchema ? { schema: fk.targetSchema } : {}
14919
+ },
14511
14920
  ...onDelete !== "NO ACTION" ? { onDelete } : {},
14512
14921
  ...onUpdate !== "NO ACTION" ? { onUpdate } : {},
14513
14922
  ...fk.deferred ? { deferred: true } : {}
@@ -14555,6 +14964,7 @@ async function introspect(pool, options) {
14555
14964
  const tablePartitions = buildPartitionMap(raw.partitions);
14556
14965
  const tableChecks = buildCheckMap(raw.checks);
14557
14966
  const tableIndexes = buildIndexMap(raw.indexes);
14967
+ const uniqueColumns = buildUniqueColumnMap(raw.uniqueColumns);
14558
14968
  const tableColumns = buildColumnMap(raw.columns);
14559
14969
  const tablePKs = buildPKMap(raw.pks);
14560
14970
  const fksByConstraint = buildFKMap(raw.fks);
@@ -14573,6 +14983,7 @@ async function introspect(pool, options) {
14573
14983
  tablePKs,
14574
14984
  fksByConstraint,
14575
14985
  tableIndexes,
14986
+ uniqueColumns,
14576
14987
  tableChecks,
14577
14988
  tableComments,
14578
14989
  columnComments,
@@ -14580,20 +14991,33 @@ async function introspect(pool, options) {
14580
14991
  tableRlsEnabled,
14581
14992
  tablePolicies,
14582
14993
  tableNames,
14994
+ schema,
14583
14995
  warnings
14584
14996
  });
14585
14997
  tables.set(tableName, table);
14586
14998
  }
14587
14999
  const extensions = raw.extensions.map((r) => r.name);
14588
15000
  const sequenceMap = buildSequenceMap(raw.sequences);
14589
- const relations = inferRelations(fksByConstraint, tableNames);
14590
- const hierarchies = detectHierarchies(tables, fksByConstraint, tableNames);
15001
+ const relations = inferRelations(fksByConstraint, tableNames, schema);
15002
+ const hierarchies = detectHierarchies(
15003
+ tables,
15004
+ fksByConstraint,
15005
+ tableNames,
15006
+ schema
15007
+ );
15008
+ const externalTables = /* @__PURE__ */ new Set();
15009
+ for (const fk of fksByConstraint.values()) {
15010
+ if (fk.targetSchema !== void 0 && fk.targetSchema !== schema && !tables.has(fk.target)) {
15011
+ externalTables.add(fk.target);
15012
+ }
15013
+ }
14591
15014
  const modelIR = new ModelIRImpl(
14592
15015
  tables,
14593
15016
  relations,
14594
15017
  enumMap,
14595
15018
  extensions,
14596
- sequenceMap
15019
+ sequenceMap,
15020
+ externalTables
14597
15021
  );
14598
15022
  return Object.assign(modelIR, {
14599
15023
  hierarchies,
@@ -14601,10 +15025,11 @@ async function introspect(pool, options) {
14601
15025
  warnings
14602
15026
  });
14603
15027
  }
14604
- function inferRelations(fksByConstraint, filteredTables) {
15028
+ function inferRelations(fksByConstraint, filteredTables, schema) {
14605
15029
  const relations = /* @__PURE__ */ new Map();
14606
15030
  const filteredSet = new Set(filteredTables);
14607
15031
  for (const [, fk] of fksByConstraint) {
15032
+ if (fk.targetSchema !== void 0 && fk.targetSchema !== schema) continue;
14608
15033
  if (!filteredSet.has(fk.source) || !filteredSet.has(fk.target)) continue;
14609
15034
  const belongsToName = deriveRelationName(fk.cols[0], fk.target);
14610
15035
  const relationFk = {
@@ -14654,13 +15079,14 @@ function deriveRelationName(fkColumn, _targetTable) {
14654
15079
  }
14655
15080
  return fkColumn;
14656
15081
  }
14657
- function detectHierarchies(tables, fksByConstraint, filteredTables) {
15082
+ function detectHierarchies(tables, fksByConstraint, filteredTables, schema) {
14658
15083
  const hierarchies = [];
14659
15084
  const filteredSet = new Set(filteredTables);
14660
15085
  const fksBySourceTarget = /* @__PURE__ */ new Map();
14661
15086
  for (const [, fk] of fksByConstraint) {
15087
+ if (fk.targetSchema !== void 0 && fk.targetSchema !== schema) continue;
14662
15088
  if (!filteredSet.has(fk.source) || !filteredSet.has(fk.target)) continue;
14663
- if (fk.source === fk.target && fk.cols.length === 1) {
15089
+ if (fk.source === fk.target && fk.cols.length === 1 && (fk.targetSchema === void 0 || fk.targetSchema === schema)) {
14664
15090
  const table = tables.get(fk.source);
14665
15091
  const pk = table?.primaryKey;
14666
15092
  const nodeIdColumn = typeof pk === "string" ? pk : pk?.[0] ?? DEFAULT_PK_COLUMN;
@@ -16935,14 +17361,16 @@ function compileJoinIntents(joins, rootTable, schemaName, deps) {
16935
17361
  schemaForFromName(schemaName, intent.table, deps.bindingNames, naming),
16936
17362
  naming
16937
17363
  );
16938
- results.push({
17364
+ const joinDecision = {
16939
17365
  type: "join",
16940
17366
  targetTable: intent.table,
16941
17367
  alias: tableAlias,
16942
17368
  joinType: intent.type,
16943
17369
  joinRarg: joinedRangeVar,
16944
- joinOnNode: onNode
16945
- });
17370
+ joinOnNode: onNode,
17371
+ joinOnParams: paramState.parameters
17372
+ };
17373
+ results.push(joinDecision);
16946
17374
  }
16947
17375
  }
16948
17376
  return results;
@@ -18149,8 +18577,9 @@ function qualifyTable3(table, schema) {
18149
18577
  return schema ? `${quoteIdent2(schema, "schema")}.${quoteIdent2(table, "table")}` : quoteIdent2(table, "table");
18150
18578
  }
18151
18579
  function generateCreateIndexSQL(table, options, schema) {
18152
- if (options.method !== void 0) {
18153
- validateIndexMethod(options.method, "index method");
18580
+ const indexMethod = options.method;
18581
+ if (indexMethod !== void 0) {
18582
+ validateIndexMethod(indexMethod, "index method");
18154
18583
  }
18155
18584
  const parts = ["CREATE"];
18156
18585
  if (options.unique) parts.push("UNIQUE");
@@ -18159,8 +18588,8 @@ function generateCreateIndexSQL(table, options, schema) {
18159
18588
  if (options.ifNotExists) parts.push("IF NOT EXISTS");
18160
18589
  parts.push(quoteIdentifier2(options.name));
18161
18590
  parts.push(`ON ${qualifyTable3(table, schema)}`);
18162
- if (options.method) {
18163
- parts.push(`USING ${options.method}`);
18591
+ if (indexMethod) {
18592
+ parts.push(`USING ${indexMethod}`);
18164
18593
  }
18165
18594
  const colParts = [];
18166
18595
  for (const col of options.columns) {
@@ -18177,13 +18606,14 @@ function generateCreateIndexSQL(table, options, schema) {
18177
18606
  if (options.with && Object.keys(options.with).length > 0) {
18178
18607
  const withParams = Object.entries(options.with).map(([k, v]) => {
18179
18608
  validateIdentifier(k, "alias");
18180
- return `${k} = ${v}`;
18609
+ return `${k} = ${formatStorageParameterValue(v, `index WITH parameter "${k}"`)}`;
18181
18610
  }).join(", ");
18182
18611
  parts.push(`WITH (${withParams})`);
18183
18612
  }
18184
- if (options.where) {
18185
- validateSqlExpression(options.where, "index WHERE predicate");
18186
- parts.push(`WHERE ${options.where}`);
18613
+ const whereExpr = options.where;
18614
+ if (whereExpr) {
18615
+ validateSqlExpression(whereExpr, "index WHERE predicate");
18616
+ parts.push(`WHERE ${whereExpr}`);
18187
18617
  }
18188
18618
  return parts.join(" ");
18189
18619
  }
@@ -18194,10 +18624,11 @@ function buildColumnPart(col, opclassMap) {
18194
18624
  if (opclass2) validateIdentifier(opclass2, "alias");
18195
18625
  return opclass2 ? `${quoted} ${opclass2}` : quoted;
18196
18626
  }
18197
- validateSqlExpression(col.expression, "index expression");
18627
+ const expression = col.expression;
18628
+ validateSqlExpression(expression, "index expression");
18198
18629
  const opclass = col.opclass;
18199
18630
  if (opclass) validateIdentifier(opclass, "alias");
18200
- return opclass ? `${col.expression} ${opclass}` : col.expression;
18631
+ return opclass ? `${expression} ${opclass}` : expression;
18201
18632
  }
18202
18633
  function generateDropIndexSQL(name, options) {
18203
18634
  const parts = ["DROP INDEX"];