@dbsp/adapter-pgsql 1.10.1 → 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]);
@@ -10747,11 +10924,9 @@ var PlanCompiler = class _PlanCompiler {
10747
10924
  }
10748
10925
  if (decision.column) {
10749
10926
  return sortBy(
10750
- columnRef(
10927
+ this.compileRelationAwareColumnRef(
10751
10928
  decision.column,
10752
- decision.table,
10753
- void 0,
10754
- this.naming
10929
+ decision.table
10755
10930
  ),
10756
10931
  decision.direction ?? "ASC",
10757
10932
  decision.nulls ?? "DEFAULT"
@@ -11010,7 +11185,7 @@ var PlanCompiler = class _PlanCompiler {
11010
11185
  // --------------------------------------------------------------------------
11011
11186
  // CASE Expression Compilation
11012
11187
  // --------------------------------------------------------------------------
11013
- compileCaseExpression(decision) {
11188
+ compileCaseExpression(decision, plan) {
11014
11189
  const conditions = decision.conditions;
11015
11190
  const elseValue = decision.value;
11016
11191
  if (!conditions || conditions.length === 0) {
@@ -11018,7 +11193,7 @@ var PlanCompiler = class _PlanCompiler {
11018
11193
  }
11019
11194
  const args = conditions.map((cond) => {
11020
11195
  const whenExpr = this.dispatchWhere(cond.when);
11021
- const thenResult = this.compileCaseValue(cond.then);
11196
+ const thenResult = this.compileCaseValue(cond.then, plan);
11022
11197
  return {
11023
11198
  CaseWhen: {
11024
11199
  expr: whenExpr,
@@ -11028,7 +11203,7 @@ var PlanCompiler = class _PlanCompiler {
11028
11203
  });
11029
11204
  let defresult;
11030
11205
  if (elseValue !== void 0) {
11031
- defresult = this.compileCaseValue(elseValue);
11206
+ defresult = this.compileCaseValue(elseValue, plan);
11032
11207
  }
11033
11208
  return {
11034
11209
  CaseExpr: {
@@ -11041,26 +11216,66 @@ var PlanCompiler = class _PlanCompiler {
11041
11216
  * Compile a CASE THEN/ELSE value based on its ExpressionIntent kind.
11042
11217
  * Delegates to shared resolveCaseValue with nested CASE support.
11043
11218
  */
11044
- compileCaseValue(value) {
11219
+ compileCaseValue(value, plan) {
11045
11220
  return resolveCaseValue(
11046
11221
  value,
11047
11222
  this.currentRootTable,
11048
11223
  void 0,
11049
11224
  this.naming,
11050
11225
  this.state,
11051
- (expr) => this.compileCaseExpression({
11052
- type: "selectExpression",
11053
- expressionType: "case",
11054
- conditions: expr.when.map((wc) => ({
11055
- when: wc.condition,
11056
- // biome-ignore lint/suspicious/noThenProperty: intentional
11057
- then: wc.result
11058
- })),
11059
- value: expr.else,
11060
- table: this.currentRootTable
11061
- })
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
+ )
11062
11244
  );
11063
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
+ }
11064
11279
  // --------------------------------------------------------------------------
11065
11280
  // INSERT Compilation
11066
11281
  // --------------------------------------------------------------------------
@@ -11263,6 +11478,9 @@ function compilePlan(plan, options) {
11263
11478
  init_naming_plugin();
11264
11479
  init_validate();
11265
11480
 
11481
+ // src/ddl/phases/comments.ts
11482
+ init_validate();
11483
+
11266
11484
  // src/ddl/phases/types.ts
11267
11485
  function sup(caps, flag) {
11268
11486
  return !caps || flag === true;
@@ -11283,6 +11501,7 @@ var VALID_INDEX_METHODS = /* @__PURE__ */ new Set([
11283
11501
  "bloom"
11284
11502
  ]);
11285
11503
  function validateIndexMethod(method, context = "index method") {
11504
+ assertString(method, `Invalid ${context}`);
11286
11505
  if (!VALID_INDEX_METHODS.has(method)) {
11287
11506
  throw new Error(
11288
11507
  `Invalid ${context}: "${method}". Must be one of: ${[...VALID_INDEX_METHODS].join(", ")}`
@@ -11290,6 +11509,7 @@ function validateIndexMethod(method, context = "index method") {
11290
11509
  }
11291
11510
  }
11292
11511
  function validateEnumLabel(value, context = "enum label") {
11512
+ assertString(value, `Invalid ${context}`);
11293
11513
  if (/\x00/.test(value)) {
11294
11514
  throw new Error(
11295
11515
  `Invalid ${context}: contains NUL byte (\\x00) which would be silently truncated by PostgreSQL`
@@ -11314,6 +11534,7 @@ function quoteCollation(name) {
11314
11534
  return `"${name}"`;
11315
11535
  }
11316
11536
  function quoteRoleName(name) {
11537
+ assertString(name, "quoteRoleName: role name");
11317
11538
  if (!name) {
11318
11539
  throw new Error("quoteRoleName: role name must not be empty");
11319
11540
  }
@@ -11380,17 +11601,21 @@ function generateCommentsPhase(ctx) {
11380
11601
  }
11381
11602
  const statements = [];
11382
11603
  for (const table of tables) {
11383
- if (table.comment) {
11604
+ const tableComment = table.comment;
11605
+ if (tableComment) {
11606
+ assertString(tableComment, "table comment");
11384
11607
  const qualifiedTable = qualifyTableIdent(table.name, schemaName, naming);
11385
11608
  statements.push(
11386
- `COMMENT ON TABLE ${qualifiedTable} IS '${table.comment.replace(/'/g, "''")}';`
11609
+ `COMMENT ON TABLE ${qualifiedTable} IS '${tableComment.replace(/'/g, "''")}';`
11387
11610
  );
11388
11611
  }
11389
11612
  for (const col of table.columns) {
11390
- if (col.comment) {
11613
+ const columnComment = col.comment;
11614
+ if (columnComment) {
11615
+ assertString(columnComment, "column comment");
11391
11616
  const qualifiedTable = qualifyTableIdent(table.name, schemaName, naming);
11392
11617
  statements.push(
11393
- `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, "''")}';`
11394
11619
  );
11395
11620
  }
11396
11621
  }
@@ -11415,9 +11640,10 @@ function generateConstraintsPhase(ctx) {
11415
11640
  for (const check of table.checkConstraints ?? []) {
11416
11641
  const qualifiedTable = qualifyTableIdent(table.name, schemaName, naming);
11417
11642
  const constraintName = quoteIdent2(check.name);
11418
- validateSqlExpression(check.expression, "check constraint expression");
11643
+ const expression = check.expression;
11644
+ validateCheckExpression(expression, "check constraint expression");
11419
11645
  statements.push(
11420
- `ALTER TABLE ${qualifiedTable} ADD CONSTRAINT ${constraintName} ${check.expression};`
11646
+ `ALTER TABLE ${qualifiedTable} ADD CONSTRAINT ${constraintName} ${expression};`
11421
11647
  );
11422
11648
  }
11423
11649
  }
@@ -11600,12 +11826,29 @@ function mapOnDeleteAction(action) {
11600
11826
  function qualifyTable(table, schemaName) {
11601
11827
  return schemaName ? `${quoteIdent2(schemaName, "schema")}.${quoteIdent2(table, "table")}` : quoteIdent2(table, "table");
11602
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
+ }
11603
11843
  function pkName(table) {
11604
11844
  return `pk_${table}`;
11605
11845
  }
11606
11846
  function fkName(table, columns) {
11607
11847
  return `fk_${table}_${columns.join("_")}`;
11608
11848
  }
11849
+ function uniqueName(table, column) {
11850
+ return `${table}_${column}_key`;
11851
+ }
11609
11852
  function idxName(table, columns, customName) {
11610
11853
  return customName ?? `idx_${table}_${columns.join("_")}`;
11611
11854
  }
@@ -11660,11 +11903,22 @@ function buildPolicySQL(tableName, policy, schemaName) {
11660
11903
  }
11661
11904
  function buildSequenceClause(verb, seqName, seq, includeCycleNoCycle = false) {
11662
11905
  const parts = [`${verb} ${seqName}`];
11663
- 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
+ );
11664
11910
  if (seq.incrementBy !== void 0)
11665
- parts.push(`INCREMENT BY ${seq.incrementBy}`);
11666
- if (seq.minValue !== void 0) parts.push(`MINVALUE ${seq.minValue}`);
11667
- 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
+ );
11668
11922
  if (includeCycleNoCycle) {
11669
11923
  if (seq.cycle !== void 0) parts.push(seq.cycle ? "CYCLE" : "NO CYCLE");
11670
11924
  } else {
@@ -11674,7 +11928,7 @@ function buildSequenceClause(verb, seqName, seq, includeCycleNoCycle = false) {
11674
11928
  }
11675
11929
  function failSafeUnknownDownChange(kind) {
11676
11930
  return {
11677
- sql: `-- WARNING: Cannot reverse unsupported SchemaChange kind "${kind}"`,
11931
+ sql: `-- WARNING: Cannot reverse unsupported SchemaChange kind "${sanitizeCommentText(kind)}"`,
11678
11932
  destructive: true
11679
11933
  };
11680
11934
  }
@@ -11736,7 +11990,7 @@ function generateMigrationSQL(diff, options) {
11736
11990
  [],
11737
11991
  // 8: alter column
11738
11992
  [],
11739
- // 9: add PK
11993
+ // 9: add PK / column UNIQUE constraint
11740
11994
  [],
11741
11995
  // 10: add FK
11742
11996
  [],
@@ -11823,6 +12077,7 @@ function getPhase(kind) {
11823
12077
  case "alter_column_collation":
11824
12078
  case "alter_column_identity":
11825
12079
  return 8;
12080
+ case "alter_column_unique":
11826
12081
  case "add_primary_key":
11827
12082
  return 9;
11828
12083
  case "add_foreign_key":
@@ -11859,7 +12114,7 @@ function upAddColumn(change, schemaName) {
11859
12114
  }
11860
12115
  function upAlterColumnType(change, schemaName) {
11861
12116
  const col = change.meta?.column;
11862
- const toType = col ? mapColumnType(col) : String(change.meta?.toType);
12117
+ const toType = col ? mapColumnType(col) : validateDbTypeName(change.meta?.toType);
11863
12118
  return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} TYPE ${toType};`;
11864
12119
  }
11865
12120
  function upAlterColumnNullable(change, schemaName) {
@@ -11874,6 +12129,34 @@ function upAlterColumnDefault(change, schemaName) {
11874
12129
  }
11875
12130
  return `ALTER TABLE ${qualifyTable(change.table, schemaName)} ALTER COLUMN ${quoteIdent2(change.column, "alias")} SET DEFAULT ${formatDefault(def)};`;
11876
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
+ }
11877
12160
  function upAddPrimaryKey(change, schemaName) {
11878
12161
  const columns = change.meta?.columns;
11879
12162
  const pkCols = columns.map((n) => quoteIdent2(n, "alias")).join(", ");
@@ -11902,8 +12185,9 @@ function upCreateIndex(change, schemaName) {
11902
12185
  "alias"
11903
12186
  );
11904
12187
  const unique = idx.unique ? "UNIQUE " : "";
11905
- if (idx.method) validateIndexMethod(idx.method, "index method");
11906
- 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}` : "";
11907
12191
  const colParts = [
11908
12192
  ...(idx.expressions ?? []).map((expr) => {
11909
12193
  validateSqlExpression(expr, "index expression");
@@ -11920,10 +12204,11 @@ function upCreateIndex(change, schemaName) {
11920
12204
  const nullsNotDistinct = idx.unique && idx.nullsNotDistinct ? " NULLS NOT DISTINCT" : "";
11921
12205
  const withParams = idx.with && Object.keys(idx.with).length > 0 ? ` WITH (${Object.entries(idx.with).map(([k, v]) => {
11922
12206
  validateIdentifier(k, "alias");
11923
- return `${k} = ${v}`;
12207
+ return `${k} = ${formatStorageParameterValue(v, `index WITH parameter "${k}"`)}`;
11924
12208
  }).join(", ")})` : "";
11925
- if (idx.where) validateSqlExpression(idx.where, "index WHERE predicate");
11926
- 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}` : "";
11927
12212
  return `CREATE ${unique}INDEX IF NOT EXISTS ${indexName} ON ${qualifyTable(change.table, schemaName)}${method} (${cols})${include}${nullsNotDistinct}${withParams}${where};`;
11928
12213
  }
11929
12214
  function upDropIndex(change, schemaName) {
@@ -11939,12 +12224,12 @@ function upDropIndex(change, schemaName) {
11939
12224
  function upAddCheckConstraint(change, schemaName) {
11940
12225
  const check = change.meta?.check;
11941
12226
  if (!check) return void 0;
12227
+ const expression = check.expression;
11942
12228
  const notValid = check.notValid ? " NOT VALID" : "";
11943
- validateSqlExpression(
11944
- check.expression,
11945
- "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"
11946
12232
  );
11947
- 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 $$;";
11948
12233
  }
11949
12234
  function upValidateConstraint(change, schemaName) {
11950
12235
  const fk = change.meta?.fk;
@@ -12013,6 +12298,7 @@ function upAlterColumnIdentity(change, schemaName) {
12013
12298
  function upAddComment(change, schemaName) {
12014
12299
  const comment = change.meta?.comment;
12015
12300
  const target = change.meta?.target;
12301
+ assertString(comment, "migration comment");
12016
12302
  const escaped = comment.replace(/'/g, "''");
12017
12303
  if (target === "table") {
12018
12304
  return `COMMENT ON TABLE ${qualifyTable(change.table, schemaName)} IS '${escaped}';`;
@@ -12040,6 +12326,8 @@ function changeToUpSQL(change, schemaName) {
12040
12326
  return upAlterColumnNullable(change, schemaName);
12041
12327
  case "alter_column_default":
12042
12328
  return upAlterColumnDefault(change, schemaName);
12329
+ case "alter_column_unique":
12330
+ return upAlterColumnUnique(change, schemaName);
12043
12331
  case "add_primary_key":
12044
12332
  return upAddPrimaryKey(change, schemaName);
12045
12333
  case "drop_primary_key":
@@ -12137,7 +12425,7 @@ function changeToDownSQL(change, schemaName) {
12137
12425
  };
12138
12426
  case "drop_table":
12139
12427
  return {
12140
- 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`,
12141
12429
  destructive: true
12142
12430
  };
12143
12431
  case "add_column":
@@ -12147,19 +12435,20 @@ function changeToDownSQL(change, schemaName) {
12147
12435
  };
12148
12436
  case "drop_column":
12149
12437
  return {
12150
- 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`,
12151
12439
  destructive: true
12152
12440
  };
12153
12441
  case "alter_column_type": {
12154
12442
  const fromType = change.meta?.fromType;
12155
- if (!fromType) {
12443
+ if (fromType == null) {
12156
12444
  return {
12157
- 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`,
12158
12446
  destructive: true
12159
12447
  };
12160
12448
  }
12449
+ const safeFromType = validateDbTypeName(fromType);
12161
12450
  return {
12162
- 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};`,
12163
12452
  destructive: true
12164
12453
  };
12165
12454
  }
@@ -12167,7 +12456,7 @@ function changeToDownSQL(change, schemaName) {
12167
12456
  const oldNullable = change.meta?.oldNullable;
12168
12457
  if (oldNullable === void 0) {
12169
12458
  return {
12170
- 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`,
12171
12460
  destructive: true
12172
12461
  };
12173
12462
  }
@@ -12182,7 +12471,7 @@ function changeToDownSQL(change, schemaName) {
12182
12471
  const oldDefault = change.meta?.oldDefault;
12183
12472
  if (oldDefault === void 0) {
12184
12473
  return {
12185
- 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`,
12186
12475
  destructive: true
12187
12476
  };
12188
12477
  }
@@ -12199,6 +12488,30 @@ function changeToDownSQL(change, schemaName) {
12199
12488
  destructive: false
12200
12489
  };
12201
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
+ }
12202
12515
  case "add_primary_key": {
12203
12516
  return {
12204
12517
  sql: `ALTER TABLE ${qualifyTable(change.table, schemaName)} DROP CONSTRAINT IF EXISTS ${quoteIdent2(pkName(change.table), "alias")} CASCADE;`,
@@ -12216,7 +12529,7 @@ function changeToDownSQL(change, schemaName) {
12216
12529
  };
12217
12530
  }
12218
12531
  return {
12219
- sql: `-- WARNING: Cannot reverse drop_primary_key "${change.table}" -- columns unknown`,
12532
+ sql: `-- WARNING: Cannot reverse drop_primary_key "${sanitizeCommentText(change.table)}" -- columns unknown`,
12220
12533
  destructive: true
12221
12534
  };
12222
12535
  }
@@ -12236,7 +12549,7 @@ function changeToDownSQL(change, schemaName) {
12236
12549
  const fk = change.meta?.fk;
12237
12550
  if (!fk) {
12238
12551
  return {
12239
- 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`,
12240
12553
  destructive: true
12241
12554
  };
12242
12555
  }
@@ -12251,7 +12564,7 @@ function changeToDownSQL(change, schemaName) {
12251
12564
  const fk = change.meta?.fk;
12252
12565
  if (!oldFk || !fk) {
12253
12566
  return {
12254
- 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`,
12255
12568
  destructive: true
12256
12569
  };
12257
12570
  }
@@ -12281,7 +12594,7 @@ ${add}`, destructive: false };
12281
12594
  const idx = change.meta?.index;
12282
12595
  if (!idx) {
12283
12596
  return {
12284
- 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`,
12285
12598
  destructive: true
12286
12599
  };
12287
12600
  }
@@ -12302,12 +12615,12 @@ ${add}`, destructive: false };
12302
12615
  case "drop_check_constraint": {
12303
12616
  const check = change.meta?.check;
12304
12617
  if (!check) return { sql: void 0, destructive: true };
12305
- validateSqlExpression(
12306
- check.expression,
12307
- "migration check constraint (down)"
12308
- );
12618
+ const expression = check.expression;
12619
+ validateCheckExpression(expression, "migration check constraint (down)");
12309
12620
  return {
12310
- 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
+ ),
12311
12624
  // Allowlisted: re-adds the dropped CHECK constraint from metadata.
12312
12625
  destructive: false
12313
12626
  };
@@ -12325,7 +12638,10 @@ ${add}`, destructive: false };
12325
12638
  const enumDef = change.meta?.enum;
12326
12639
  if (!enumDef) return { sql: void 0, destructive: true };
12327
12640
  const enumName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(enumDef.name, "alias")}` : quoteIdent2(enumDef.name, "alias");
12328
- 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(", ");
12329
12645
  return {
12330
12646
  sql: `CREATE TYPE ${enumName} AS ENUM (${values});`,
12331
12647
  // Allowlisted: re-creates the dropped enum type from metadata.
@@ -12339,7 +12655,7 @@ ${add}`, destructive: false };
12339
12655
  };
12340
12656
  case "alter_column_collation": {
12341
12657
  return {
12342
- 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`,
12343
12659
  destructive: true
12344
12660
  };
12345
12661
  }
@@ -12365,7 +12681,7 @@ ${add}`, destructive: false };
12365
12681
  }
12366
12682
  if (!prevIdentity && !col.identity) {
12367
12683
  return {
12368
- 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`,
12369
12685
  destructive: true
12370
12686
  };
12371
12687
  }
@@ -12392,7 +12708,8 @@ ${add}`, destructive: false };
12392
12708
  case "drop_comment": {
12393
12709
  const target = change.meta?.target;
12394
12710
  const comment = change.meta?.comment;
12395
- if (typeof comment === "string") {
12711
+ if (comment !== void 0) {
12712
+ assertString(comment, "migration comment");
12396
12713
  const escaped = comment.replace(/'/g, "''");
12397
12714
  if (target === "table") {
12398
12715
  return {
@@ -12409,8 +12726,9 @@ ${add}`, destructive: false };
12409
12726
  };
12410
12727
  }
12411
12728
  }
12729
+ const columnTarget2 = change.column === void 0 ? "" : `."${sanitizeCommentText(change.column)}"`;
12412
12730
  return {
12413
- 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",
12414
12732
  destructive: true
12415
12733
  };
12416
12734
  }
@@ -12444,7 +12762,7 @@ ${add}`, destructive: false };
12444
12762
  const prevSeq = change.meta?.previousSequence;
12445
12763
  if (!prevSeq) {
12446
12764
  return {
12447
- sql: `-- WARNING: Cannot reverse alter_sequence "${change.table}" -- missing migration metadata`,
12765
+ sql: `-- WARNING: Cannot reverse alter_sequence "${sanitizeCommentText(change.table)}" -- missing migration metadata`,
12448
12766
  destructive: true
12449
12767
  };
12450
12768
  }
@@ -12467,7 +12785,7 @@ ${add}`, destructive: false };
12467
12785
  }
12468
12786
  case "validate_constraint":
12469
12787
  return {
12470
- sql: `-- VALIDATE CONSTRAINT cannot be reversed in PostgreSQL (table: "${change.table}")`,
12788
+ sql: `-- VALIDATE CONSTRAINT cannot be reversed in PostgreSQL (table: "${sanitizeCommentText(change.table)}")`,
12471
12789
  destructive: true
12472
12790
  };
12473
12791
  case "enable_rls":
@@ -12508,7 +12826,6 @@ function generateDownSQL(diff, options) {
12508
12826
  function generateDownMigrationSQL(diff, options) {
12509
12827
  const schemaName = options?.schemaName;
12510
12828
  const includeDestructive = options?.includeDestructive ?? true;
12511
- const changes = includeDestructive ? diff.changes : diff.changes.filter((c) => !c.destructive);
12512
12829
  const phases = [
12513
12830
  [],
12514
12831
  // 0: drop FK, drop CHECK
@@ -12529,7 +12846,7 @@ function generateDownMigrationSQL(diff, options) {
12529
12846
  [],
12530
12847
  // 8: alter column
12531
12848
  [],
12532
- // 9: add PK
12849
+ // 9: add PK / column UNIQUE constraint
12533
12850
  [],
12534
12851
  // 10: add FK
12535
12852
  [],
@@ -12549,7 +12866,7 @@ function generateDownMigrationSQL(diff, options) {
12549
12866
  []
12550
12867
  // 18: CREATE/DROP POLICY
12551
12868
  ];
12552
- for (const change of changes) {
12869
+ for (const change of diff.changes) {
12553
12870
  const phase = getPhase(change.kind);
12554
12871
  phases[phase].push(change);
12555
12872
  }
@@ -12558,6 +12875,7 @@ function generateDownMigrationSQL(diff, options) {
12558
12875
  for (let i = phases.length - 1; i >= 0; i--) {
12559
12876
  for (const change of phases[i]) {
12560
12877
  const down = changeToDownSQL(change, schemaName);
12878
+ if (!includeDestructive && down.destructive) continue;
12561
12879
  destructive = down.destructive || destructive;
12562
12880
  if (down.sql) statements.push(down.sql);
12563
12881
  }
@@ -12602,7 +12920,10 @@ function generateAddFKSQL(tableName, fk, schemaName) {
12602
12920
  const qualTable = qualifyTable(tableName, schemaName);
12603
12921
  const constraintName = quoteIdent2(fkName(tableName, fk.columns), "alias");
12604
12922
  const fkCols = fk.columns.map((n) => quoteIdent2(n, "alias")).join(", ");
12605
- const refTable = qualifyTable(fk.references.table, schemaName);
12923
+ const refTable = qualifyTable(
12924
+ fk.references.table,
12925
+ fk.references.schema ?? schemaName
12926
+ );
12606
12927
  const refCols = fk.references.columns.map((n) => quoteIdent2(n, "alias")).join(", ");
12607
12928
  const onDelete = fk.onDelete ? ` ON DELETE ${mapOnDeleteAction(fk.onDelete)}` : "";
12608
12929
  const onUpdate = fk.onUpdate ? ` ON UPDATE ${mapOnDeleteAction(fk.onUpdate)}` : "";
@@ -12688,10 +13009,11 @@ function quoteIdentifier(name) {
12688
13009
  validateIdentifier(name, "alias");
12689
13010
  return `"${name.replace(/"/g, '""')}"`;
12690
13011
  }
12691
- function qualifyTable2(tableName, schemaName, naming) {
13012
+ function qualifyTable2(tableName, schemaName, naming, transformSchema = true) {
12692
13013
  const table = quoteIdentifier(naming.toDatabase(tableName));
12693
13014
  if (schemaName) {
12694
- return `${quoteIdentifier(naming.toDatabase(schemaName))}.${table}`;
13015
+ const schema = transformSchema ? naming.toDatabase(schemaName) : schemaName;
13016
+ return `${quoteIdentifier(schema)}.${table}`;
12695
13017
  }
12696
13018
  return table;
12697
13019
  }
@@ -12762,7 +13084,7 @@ function generateAlterTableAddFK(tableName, fk, schemaName, naming) {
12762
13084
  `fk_${tableName}_${fk.columns.join("_")}`
12763
13085
  );
12764
13086
  const fkCols = fk.columns.map((col) => quoteIdentifier(naming.toDatabase(col))).join(", ");
12765
- 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);
12766
13088
  const refCols = fk.references.columns.map((col) => quoteIdentifier(naming.toDatabase(col))).join(", ");
12767
13089
  const onDelete = fk.onDelete ? ` ON DELETE ${mapOnDeleteAction(fk.onDelete)}` : "";
12768
13090
  const onUpdate = fk.onUpdate ? ` ON UPDATE ${mapOnDeleteAction(fk.onUpdate)}` : "";
@@ -12776,8 +13098,9 @@ function generateCreateIndex(tableName, idx, schemaName, naming) {
12776
13098
  );
12777
13099
  const qualifiedTable = qualifyTable2(tableName, schemaName, naming);
12778
13100
  const unique = idx.unique ? "UNIQUE " : "";
12779
- if (idx.method) validateIndexMethod(idx.method, "index method");
12780
- 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}` : "";
12781
13104
  const colParts = [];
12782
13105
  if (idx.expressions && idx.expressions.length > 0) {
12783
13106
  for (const expr of idx.expressions) {
@@ -12797,10 +13120,11 @@ function generateCreateIndex(tableName, idx, schemaName, naming) {
12797
13120
  const nullsNotDistinct = idx.unique && idx.nullsNotDistinct ? " NULLS NOT DISTINCT" : "";
12798
13121
  const withParams = idx.with && Object.keys(idx.with).length > 0 ? ` WITH (${Object.entries(idx.with).map(([k, v]) => {
12799
13122
  validateIdentifier(k, "alias");
12800
- return `${k} = ${v}`;
13123
+ return `${k} = ${formatStorageParameterValue(v, `index WITH parameter "${k}"`)}`;
12801
13124
  }).join(", ")})` : "";
12802
- if (idx.where) validateSqlExpression(idx.where, "index WHERE predicate");
12803
- 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}` : "";
12804
13128
  return `CREATE ${unique}INDEX ${indexName} ON ${qualifiedTable}${method} (${cols})${include}${nullsNotDistinct}${withParams}${where};`;
12805
13129
  }
12806
13130
  function generateCreatePolicy(tableName, policy, schemaName, naming) {
@@ -13167,7 +13491,8 @@ function normalizeTable(table, plugin) {
13167
13491
  columns: fk.columns.map(toDb),
13168
13492
  references: {
13169
13493
  table: toDb(fk.references.table),
13170
- columns: fk.references.columns.map(toDb)
13494
+ columns: fk.references.columns.map(toDb),
13495
+ ...fk.references.schema !== void 0 ? { schema: fk.references.schema } : {}
13171
13496
  }
13172
13497
  })),
13173
13498
  indexes: table.indexes.map((idx) => ({
@@ -13260,6 +13585,21 @@ function compareColumnDetails(tableName, schema, db, changes) {
13260
13585
  meta: { default: schema.default, oldDefault: db.default }
13261
13586
  });
13262
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
+ }
13263
13603
  if ((schema.collation ?? null) !== (db.collation ?? null)) {
13264
13604
  changes.push({
13265
13605
  kind: "alter_column_collation",
@@ -13331,7 +13671,8 @@ function comparePrimaryKeys(schema, db, changes) {
13331
13671
  kind: "drop_primary_key",
13332
13672
  table: schema.name,
13333
13673
  destructive: true,
13334
- details: `Drop primary key (${dbPK.join(", ")})`
13674
+ details: `Drop primary key (${dbPK.join(", ")})`,
13675
+ meta: { columns: dbPK }
13335
13676
  });
13336
13677
  return;
13337
13678
  }
@@ -13339,7 +13680,8 @@ function comparePrimaryKeys(schema, db, changes) {
13339
13680
  kind: "drop_primary_key",
13340
13681
  table: schema.name,
13341
13682
  destructive: true,
13342
- details: `Drop primary key (${dbPK.join(", ")})`
13683
+ details: `Drop primary key (${dbPK.join(", ")})`,
13684
+ meta: { columns: dbPK }
13343
13685
  });
13344
13686
  changes.push({
13345
13687
  kind: "add_primary_key",
@@ -13411,7 +13753,7 @@ function compareForeignKeys(schema, db, changes) {
13411
13753
  }
13412
13754
  }
13413
13755
  function fkKey(fk) {
13414
- 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(",")})`;
13415
13757
  }
13416
13758
  function compareIndexes(schema, db, changes) {
13417
13759
  const explicitIndexCols = new Set(
@@ -13444,16 +13786,15 @@ function compareIndexes(schema, db, changes) {
13444
13786
  });
13445
13787
  }
13446
13788
  }
13447
- const autoUniqueIndexKeys = new Set(
13448
- schema.columns.filter((col) => col.unique === true).map(
13449
- (col) => indexKey({
13450
- columns: [col.name],
13451
- unique: true
13452
- })
13453
- )
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)
13454
13795
  );
13455
13796
  for (const [key, idx] of dbIdxMap) {
13456
- if (!schemaIdxMap.has(key) && !autoFkIndexKeys.has(key) && !autoUniqueIndexKeys.has(key)) {
13797
+ if (!schemaIdxMap.has(key) && !autoFkIndexKeys.has(key) && !isAutoUniqueIndex(schema.name, idx, autoUniqueIndexColumns)) {
13457
13798
  changes.push({
13458
13799
  kind: "drop_index",
13459
13800
  table: schema.name,
@@ -13464,6 +13805,16 @@ function compareIndexes(schema, db, changes) {
13464
13805
  }
13465
13806
  }
13466
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
+ }
13467
13818
  function indexKey(idx) {
13468
13819
  const parts = [
13469
13820
  idx.columns.join(","),
@@ -13828,6 +14179,7 @@ function buildSummary(changes) {
13828
14179
  case "alter_column_type":
13829
14180
  case "alter_column_nullable":
13830
14181
  case "alter_column_default":
14182
+ case "alter_column_unique":
13831
14183
  columns.altered++;
13832
14184
  break;
13833
14185
  case "add_primary_key":
@@ -14078,6 +14430,7 @@ async function queryAllCatalogs(pool, schema) {
14078
14430
  pksResult,
14079
14431
  fksResult,
14080
14432
  indexesResult,
14433
+ uniqueColumnsResult,
14081
14434
  enumsResult,
14082
14435
  commentsResult,
14083
14436
  checksResult,
@@ -14111,28 +14464,44 @@ async function queryAllCatalogs(pool, schema) {
14111
14464
  // 3. Foreign keys
14112
14465
  pool.query(
14113
14466
  `SELECT
14114
- tc.constraint_name,
14115
- kcu.table_name AS source_table,
14116
- kcu.column_name AS source_column,
14117
- ccu.table_name AS target_table,
14118
- ccu.column_name AS target_column,
14119
- rc.delete_rule,
14120
- rc.update_rule,
14121
- tc.is_deferrable,
14122
- tc.initially_deferred
14123
- FROM information_schema.table_constraints tc
14124
- JOIN information_schema.key_column_usage kcu
14125
- ON tc.constraint_name = kcu.constraint_name
14126
- AND tc.table_schema = kcu.table_schema
14127
- JOIN information_schema.constraint_column_usage ccu
14128
- ON ccu.constraint_name = tc.constraint_name
14129
- AND ccu.table_schema = tc.table_schema
14130
- JOIN information_schema.referential_constraints rc
14131
- ON rc.constraint_name = tc.constraint_name
14132
- AND rc.constraint_schema = tc.table_schema
14133
- WHERE tc.constraint_type = 'FOREIGN KEY'
14134
- AND tc.table_schema = $1
14135
- 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`,
14136
14505
  [schema]
14137
14506
  ),
14138
14507
  // 4. Indexes (excluding PK-backing indexes and unique-constraint-backing indexes).
@@ -14196,7 +14565,19 @@ async function queryAllCatalogs(pool, schema) {
14196
14565
  ORDER BY t.relname, i.relname`,
14197
14566
  [schema]
14198
14567
  ),
14199
- // 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
14200
14581
  pool.query(
14201
14582
  `SELECT
14202
14583
  t.typname AS name,
@@ -14209,7 +14590,7 @@ async function queryAllCatalogs(pool, schema) {
14209
14590
  GROUP BY t.typname`,
14210
14591
  [schema]
14211
14592
  ),
14212
- // 6. Comments (table and column level) from pg_description
14593
+ // 7. Comments (table and column level) from pg_description
14213
14594
  pool.query(
14214
14595
  `SELECT
14215
14596
  c.relname AS table_name,
@@ -14223,7 +14604,7 @@ async function queryAllCatalogs(pool, schema) {
14223
14604
  AND d.objsubid >= 0`,
14224
14605
  [schema]
14225
14606
  ),
14226
- // 7. CHECK constraints
14607
+ // 8. CHECK constraints
14227
14608
  pool.query(
14228
14609
  `SELECT
14229
14610
  c.conname AS name,
@@ -14235,7 +14616,7 @@ async function queryAllCatalogs(pool, schema) {
14235
14616
  AND n.nspname = $1`,
14236
14617
  [schema]
14237
14618
  ),
14238
- // 8. Partition configurations
14619
+ // 9. Partition configurations
14239
14620
  pool.query(
14240
14621
  `SELECT
14241
14622
  c.relname AS table_name,
@@ -14250,13 +14631,13 @@ async function queryAllCatalogs(pool, schema) {
14250
14631
  GROUP BY c.relname, p.partstrat`,
14251
14632
  [schema]
14252
14633
  ),
14253
- // 9. Installed extensions (no schema param — queries globally; skip plpgsql)
14634
+ // 10. Installed extensions (no schema param — queries globally; skip plpgsql)
14254
14635
  pool.query(
14255
14636
  `SELECT extname AS name
14256
14637
  FROM pg_extension
14257
14638
  WHERE extname != 'plpgsql'`
14258
14639
  ),
14259
- // 10. Sequences not backed by SERIAL
14640
+ // 11. Sequences not backed by SERIAL
14260
14641
  pool.query(
14261
14642
  `SELECT s.sequencename AS name, s.start_value, s.increment_by, s.min_value, s.max_value, s.cycle
14262
14643
  FROM pg_sequences s
@@ -14267,7 +14648,7 @@ async function queryAllCatalogs(pool, schema) {
14267
14648
  AND d.objid IS NULL`,
14268
14649
  [schema]
14269
14650
  ),
14270
- // 11. RLS enabled state per table
14651
+ // 12. RLS enabled state per table
14271
14652
  pool.query(
14272
14653
  `SELECT c.relname AS table_name, c.relrowsecurity AS rls_enabled
14273
14654
  FROM pg_class c
@@ -14276,7 +14657,7 @@ async function queryAllCatalogs(pool, schema) {
14276
14657
  AND c.relkind = 'r'`,
14277
14658
  [schema]
14278
14659
  ),
14279
- // 12. Row-Level Security policies
14660
+ // 13. Row-Level Security policies
14280
14661
  pool.query(
14281
14662
  `SELECT
14282
14663
  c.relname AS table_name,
@@ -14298,6 +14679,7 @@ async function queryAllCatalogs(pool, schema) {
14298
14679
  pks: pksResult.rows,
14299
14680
  fks: fksResult.rows,
14300
14681
  indexes: indexesResult.rows,
14682
+ uniqueColumns: uniqueColumnsResult.rows,
14301
14683
  enums: enumsResult.rows,
14302
14684
  comments: commentsResult.rows,
14303
14685
  checks: checksResult.rows,
@@ -14383,6 +14765,21 @@ function buildIndexMap(rows) {
14383
14765
  }
14384
14766
  return result;
14385
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
+ }
14386
14783
  function buildColumnMap(rows) {
14387
14784
  const result = /* @__PURE__ */ new Map();
14388
14785
  for (const col of rows) {
@@ -14410,14 +14807,16 @@ function buildPKMap(rows) {
14410
14807
  function buildFKMap(rows) {
14411
14808
  const result = /* @__PURE__ */ new Map();
14412
14809
  for (const fk of rows) {
14413
- const existing = result.get(fk.constraint_name);
14810
+ const key = JSON.stringify([fk.source_table, fk.constraint_name]);
14811
+ const existing = result.get(key);
14414
14812
  if (existing) {
14415
14813
  existing.cols.push(fk.source_column);
14416
14814
  existing.refs.push(fk.target_column);
14417
14815
  } else {
14418
- result.set(fk.constraint_name, {
14816
+ result.set(key, {
14419
14817
  source: fk.source_table,
14420
14818
  target: fk.target_table,
14819
+ targetSchema: fk.target_schema,
14421
14820
  cols: [fk.source_column],
14422
14821
  refs: [fk.target_column],
14423
14822
  deleteRule: fk.delete_rule,
@@ -14483,10 +14882,12 @@ function buildRLSMaps(rlsRows, policiesRows) {
14483
14882
  function buildTableIR(tableName, ctx) {
14484
14883
  const rawCols = ctx.tableColumns.get(tableName) ?? [];
14485
14884
  const pkCols = ctx.tablePKs.get(tableName);
14885
+ const uniqueColumns = ctx.uniqueColumns.get(tableName);
14486
14886
  const columns = rawCols.map((col) => {
14487
14887
  const identity = col.is_identity === "YES" && col.identity_generation ? col.identity_generation === "ALWAYS" ? "always" : "byDefault" : void 0;
14488
14888
  const collation = col.collation_name && col.collation_name !== "default" ? col.collation_name : void 0;
14489
14889
  const colComment = ctx.columnComments.get(`${tableName}.${col.column_name}`) ?? void 0;
14890
+ const uniqueConstraintName = uniqueColumns?.get(col.column_name);
14490
14891
  return {
14491
14892
  name: col.column_name,
14492
14893
  type: mapPgType(col.data_type, col.udt_name),
@@ -14496,6 +14897,7 @@ function buildTableIR(tableName, ctx) {
14496
14897
  // of quoting it as a string literal.
14497
14898
  ...col.column_default != null ? { default: { sql: col.column_default } } : {},
14498
14899
  originalDbType: col.udt_name,
14900
+ ...uniqueConstraintName !== void 0 ? { unique: true, uniqueConstraintName } : {},
14499
14901
  ...collation ? { collation } : {},
14500
14902
  ...identity ? { identity } : {},
14501
14903
  ...colComment ? { comment: colComment } : {}
@@ -14504,12 +14906,17 @@ function buildTableIR(tableName, ctx) {
14504
14906
  const foreignKeys = [];
14505
14907
  for (const [, fk] of ctx.fksByConstraint) {
14506
14908
  if (fk.source !== tableName) continue;
14507
- 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;
14508
14911
  const onDelete = mapDeleteRule(fk.deleteRule);
14509
14912
  const onUpdate = mapDeleteRule(fk.updateRule);
14510
14913
  foreignKeys.push({
14511
14914
  columns: fk.cols,
14512
- references: { table: fk.target, columns: fk.refs },
14915
+ references: {
14916
+ table: fk.target,
14917
+ columns: fk.refs,
14918
+ ...isCrossSchema ? { schema: fk.targetSchema } : {}
14919
+ },
14513
14920
  ...onDelete !== "NO ACTION" ? { onDelete } : {},
14514
14921
  ...onUpdate !== "NO ACTION" ? { onUpdate } : {},
14515
14922
  ...fk.deferred ? { deferred: true } : {}
@@ -14557,6 +14964,7 @@ async function introspect(pool, options) {
14557
14964
  const tablePartitions = buildPartitionMap(raw.partitions);
14558
14965
  const tableChecks = buildCheckMap(raw.checks);
14559
14966
  const tableIndexes = buildIndexMap(raw.indexes);
14967
+ const uniqueColumns = buildUniqueColumnMap(raw.uniqueColumns);
14560
14968
  const tableColumns = buildColumnMap(raw.columns);
14561
14969
  const tablePKs = buildPKMap(raw.pks);
14562
14970
  const fksByConstraint = buildFKMap(raw.fks);
@@ -14575,6 +14983,7 @@ async function introspect(pool, options) {
14575
14983
  tablePKs,
14576
14984
  fksByConstraint,
14577
14985
  tableIndexes,
14986
+ uniqueColumns,
14578
14987
  tableChecks,
14579
14988
  tableComments,
14580
14989
  columnComments,
@@ -14582,20 +14991,33 @@ async function introspect(pool, options) {
14582
14991
  tableRlsEnabled,
14583
14992
  tablePolicies,
14584
14993
  tableNames,
14994
+ schema,
14585
14995
  warnings
14586
14996
  });
14587
14997
  tables.set(tableName, table);
14588
14998
  }
14589
14999
  const extensions = raw.extensions.map((r) => r.name);
14590
15000
  const sequenceMap = buildSequenceMap(raw.sequences);
14591
- const relations = inferRelations(fksByConstraint, tableNames);
14592
- 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
+ }
14593
15014
  const modelIR = new ModelIRImpl(
14594
15015
  tables,
14595
15016
  relations,
14596
15017
  enumMap,
14597
15018
  extensions,
14598
- sequenceMap
15019
+ sequenceMap,
15020
+ externalTables
14599
15021
  );
14600
15022
  return Object.assign(modelIR, {
14601
15023
  hierarchies,
@@ -14603,10 +15025,11 @@ async function introspect(pool, options) {
14603
15025
  warnings
14604
15026
  });
14605
15027
  }
14606
- function inferRelations(fksByConstraint, filteredTables) {
15028
+ function inferRelations(fksByConstraint, filteredTables, schema) {
14607
15029
  const relations = /* @__PURE__ */ new Map();
14608
15030
  const filteredSet = new Set(filteredTables);
14609
15031
  for (const [, fk] of fksByConstraint) {
15032
+ if (fk.targetSchema !== void 0 && fk.targetSchema !== schema) continue;
14610
15033
  if (!filteredSet.has(fk.source) || !filteredSet.has(fk.target)) continue;
14611
15034
  const belongsToName = deriveRelationName(fk.cols[0], fk.target);
14612
15035
  const relationFk = {
@@ -14656,13 +15079,14 @@ function deriveRelationName(fkColumn, _targetTable) {
14656
15079
  }
14657
15080
  return fkColumn;
14658
15081
  }
14659
- function detectHierarchies(tables, fksByConstraint, filteredTables) {
15082
+ function detectHierarchies(tables, fksByConstraint, filteredTables, schema) {
14660
15083
  const hierarchies = [];
14661
15084
  const filteredSet = new Set(filteredTables);
14662
15085
  const fksBySourceTarget = /* @__PURE__ */ new Map();
14663
15086
  for (const [, fk] of fksByConstraint) {
15087
+ if (fk.targetSchema !== void 0 && fk.targetSchema !== schema) continue;
14664
15088
  if (!filteredSet.has(fk.source) || !filteredSet.has(fk.target)) continue;
14665
- 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)) {
14666
15090
  const table = tables.get(fk.source);
14667
15091
  const pk = table?.primaryKey;
14668
15092
  const nodeIdColumn = typeof pk === "string" ? pk : pk?.[0] ?? DEFAULT_PK_COLUMN;
@@ -16065,10 +16489,9 @@ function convertDottedFieldsToExists(decisions, rootTable, model) {
16065
16489
  ...resolved.relationType && { relationType: resolved.relationType },
16066
16490
  conditions: [
16067
16491
  {
16492
+ ...d,
16068
16493
  type: "where",
16069
16494
  column: targetColumn,
16070
- operator: d.operator,
16071
- value: d.value,
16072
16495
  table: resolved.target
16073
16496
  }
16074
16497
  ]
@@ -16938,14 +17361,16 @@ function compileJoinIntents(joins, rootTable, schemaName, deps) {
16938
17361
  schemaForFromName(schemaName, intent.table, deps.bindingNames, naming),
16939
17362
  naming
16940
17363
  );
16941
- results.push({
17364
+ const joinDecision = {
16942
17365
  type: "join",
16943
17366
  targetTable: intent.table,
16944
17367
  alias: tableAlias,
16945
17368
  joinType: intent.type,
16946
17369
  joinRarg: joinedRangeVar,
16947
- joinOnNode: onNode
16948
- });
17370
+ joinOnNode: onNode,
17371
+ joinOnParams: paramState.parameters
17372
+ };
17373
+ results.push(joinDecision);
16949
17374
  }
16950
17375
  }
16951
17376
  return results;
@@ -18152,8 +18577,9 @@ function qualifyTable3(table, schema) {
18152
18577
  return schema ? `${quoteIdent2(schema, "schema")}.${quoteIdent2(table, "table")}` : quoteIdent2(table, "table");
18153
18578
  }
18154
18579
  function generateCreateIndexSQL(table, options, schema) {
18155
- if (options.method !== void 0) {
18156
- validateIndexMethod(options.method, "index method");
18580
+ const indexMethod = options.method;
18581
+ if (indexMethod !== void 0) {
18582
+ validateIndexMethod(indexMethod, "index method");
18157
18583
  }
18158
18584
  const parts = ["CREATE"];
18159
18585
  if (options.unique) parts.push("UNIQUE");
@@ -18162,8 +18588,8 @@ function generateCreateIndexSQL(table, options, schema) {
18162
18588
  if (options.ifNotExists) parts.push("IF NOT EXISTS");
18163
18589
  parts.push(quoteIdentifier2(options.name));
18164
18590
  parts.push(`ON ${qualifyTable3(table, schema)}`);
18165
- if (options.method) {
18166
- parts.push(`USING ${options.method}`);
18591
+ if (indexMethod) {
18592
+ parts.push(`USING ${indexMethod}`);
18167
18593
  }
18168
18594
  const colParts = [];
18169
18595
  for (const col of options.columns) {
@@ -18180,13 +18606,14 @@ function generateCreateIndexSQL(table, options, schema) {
18180
18606
  if (options.with && Object.keys(options.with).length > 0) {
18181
18607
  const withParams = Object.entries(options.with).map(([k, v]) => {
18182
18608
  validateIdentifier(k, "alias");
18183
- return `${k} = ${v}`;
18609
+ return `${k} = ${formatStorageParameterValue(v, `index WITH parameter "${k}"`)}`;
18184
18610
  }).join(", ");
18185
18611
  parts.push(`WITH (${withParams})`);
18186
18612
  }
18187
- if (options.where) {
18188
- validateSqlExpression(options.where, "index WHERE predicate");
18189
- parts.push(`WHERE ${options.where}`);
18613
+ const whereExpr = options.where;
18614
+ if (whereExpr) {
18615
+ validateSqlExpression(whereExpr, "index WHERE predicate");
18616
+ parts.push(`WHERE ${whereExpr}`);
18190
18617
  }
18191
18618
  return parts.join(" ");
18192
18619
  }
@@ -18197,10 +18624,11 @@ function buildColumnPart(col, opclassMap) {
18197
18624
  if (opclass2) validateIdentifier(opclass2, "alias");
18198
18625
  return opclass2 ? `${quoted} ${opclass2}` : quoted;
18199
18626
  }
18200
- validateSqlExpression(col.expression, "index expression");
18627
+ const expression = col.expression;
18628
+ validateSqlExpression(expression, "index expression");
18201
18629
  const opclass = col.opclass;
18202
18630
  if (opclass) validateIdentifier(opclass, "alias");
18203
- return opclass ? `${col.expression} ${opclass}` : col.expression;
18631
+ return opclass ? `${expression} ${opclass}` : expression;
18204
18632
  }
18205
18633
  function generateDropIndexSQL(name, options) {
18206
18634
  const parts = ["DROP INDEX"];