turbine-orm 0.65.0 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +34 -32
  2. package/dist/adapters/cockroachdb.js +21 -3
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/yugabytedb.js +20 -3
  5. package/dist/cjs/adapters/cockroachdb.js +21 -3
  6. package/dist/cjs/adapters/index.d.ts +15 -0
  7. package/dist/cjs/adapters/yugabytedb.js +20 -3
  8. package/dist/cjs/cli/destructive.d.ts +18 -4
  9. package/dist/cjs/cli/destructive.js +230 -122
  10. package/dist/cjs/cli/index.d.ts +21 -4
  11. package/dist/cjs/cli/index.js +119 -22
  12. package/dist/cjs/cli/mcp.d.ts +28 -8
  13. package/dist/cjs/cli/mcp.js +170 -127
  14. package/dist/cjs/cli/migrate.d.ts +134 -13
  15. package/dist/cjs/cli/migrate.js +349 -241
  16. package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
  17. package/dist/cjs/cli/pii-predicate-guard.js +390 -0
  18. package/dist/cjs/cli/prisma-resolve.js +75 -4
  19. package/dist/cjs/cli/prisma-schema.d.ts +17 -1
  20. package/dist/cjs/cli/prisma-schema.js +83 -17
  21. package/dist/cjs/cli/sql-statements.d.ts +125 -0
  22. package/dist/cjs/cli/sql-statements.js +378 -0
  23. package/dist/cjs/cli/studio.js +49 -118
  24. package/dist/cjs/cli/ui.d.ts +1 -1
  25. package/dist/cjs/client.d.ts +43 -0
  26. package/dist/cjs/client.js +125 -6
  27. package/dist/cjs/dialect.d.ts +123 -0
  28. package/dist/cjs/dialect.js +33 -0
  29. package/dist/cjs/errors.d.ts +74 -1
  30. package/dist/cjs/errors.js +239 -25
  31. package/dist/cjs/index-advisor.d.ts +33 -1
  32. package/dist/cjs/index-advisor.js +32 -1
  33. package/dist/cjs/introspect.d.ts +48 -0
  34. package/dist/cjs/introspect.js +222 -91
  35. package/dist/cjs/mssql.js +43 -1
  36. package/dist/cjs/mysql.d.ts +5 -2
  37. package/dist/cjs/mysql.js +202 -17
  38. package/dist/cjs/nested-write.js +6 -1
  39. package/dist/cjs/pipeline-submittable.js +17 -3
  40. package/dist/cjs/pipeline.js +75 -9
  41. package/dist/cjs/powdb.d.ts +23 -0
  42. package/dist/cjs/powdb.js +33 -1
  43. package/dist/cjs/powql.d.ts +61 -9
  44. package/dist/cjs/powql.js +186 -49
  45. package/dist/cjs/prisma-compat.js +160 -41
  46. package/dist/cjs/query/aggregates.d.ts +1 -1
  47. package/dist/cjs/query/aggregates.js +80 -18
  48. package/dist/cjs/query/batched-loader.d.ts +10 -0
  49. package/dist/cjs/query/batched-loader.js +268 -7
  50. package/dist/cjs/query/builder.d.ts +73 -0
  51. package/dist/cjs/query/builder.js +225 -28
  52. package/dist/cjs/query/filters.d.ts +162 -0
  53. package/dist/cjs/query/filters.js +250 -1
  54. package/dist/cjs/query/relations.d.ts +10 -10
  55. package/dist/cjs/query/relations.js +93 -12
  56. package/dist/cjs/query/types.d.ts +14 -1
  57. package/dist/cjs/query/utils.d.ts +146 -2
  58. package/dist/cjs/query/utils.js +210 -4
  59. package/dist/cjs/query/warn-registry.d.ts +10 -0
  60. package/dist/cjs/query/warn-registry.js +10 -0
  61. package/dist/cjs/query/where-compile.d.ts +30 -0
  62. package/dist/cjs/query/where-compile.js +41 -0
  63. package/dist/cjs/query/where.d.ts +128 -13
  64. package/dist/cjs/query/where.js +215 -77
  65. package/dist/cjs/query/writes.d.ts +1 -1
  66. package/dist/cjs/query/writes.js +39 -15
  67. package/dist/cjs/schema-builder.d.ts +2 -1
  68. package/dist/cjs/schema-sql.d.ts +94 -4
  69. package/dist/cjs/schema-sql.js +506 -30
  70. package/dist/cjs/schema.d.ts +3 -1
  71. package/dist/cjs/sqlite.d.ts +6 -0
  72. package/dist/cjs/sqlite.js +151 -10
  73. package/dist/cjs/typed-sql.d.ts +29 -1
  74. package/dist/cjs/typed-sql.js +30 -12
  75. package/dist/cli/destructive.d.ts +18 -4
  76. package/dist/cli/destructive.js +229 -121
  77. package/dist/cli/index.d.ts +21 -4
  78. package/dist/cli/index.js +120 -24
  79. package/dist/cli/mcp.d.ts +28 -8
  80. package/dist/cli/mcp.js +172 -129
  81. package/dist/cli/migrate.d.ts +134 -13
  82. package/dist/cli/migrate.js +347 -238
  83. package/dist/cli/pii-predicate-guard.d.ts +112 -0
  84. package/dist/cli/pii-predicate-guard.js +386 -0
  85. package/dist/cli/prisma-resolve.js +75 -4
  86. package/dist/cli/prisma-schema.d.ts +17 -1
  87. package/dist/cli/prisma-schema.js +83 -17
  88. package/dist/cli/sql-statements.d.ts +125 -0
  89. package/dist/cli/sql-statements.js +373 -0
  90. package/dist/cli/studio.js +49 -118
  91. package/dist/cli/ui.d.ts +1 -1
  92. package/dist/client.d.ts +43 -0
  93. package/dist/client.js +126 -7
  94. package/dist/dialect.d.ts +123 -0
  95. package/dist/dialect.js +33 -0
  96. package/dist/errors.d.ts +74 -1
  97. package/dist/errors.js +228 -19
  98. package/dist/index-advisor.d.ts +33 -1
  99. package/dist/index-advisor.js +31 -1
  100. package/dist/introspect.d.ts +48 -0
  101. package/dist/introspect.js +221 -91
  102. package/dist/mssql.js +44 -2
  103. package/dist/mysql.d.ts +5 -2
  104. package/dist/mysql.js +203 -18
  105. package/dist/nested-write.js +7 -2
  106. package/dist/pipeline-submittable.js +18 -4
  107. package/dist/pipeline.js +76 -10
  108. package/dist/powdb.d.ts +23 -0
  109. package/dist/powdb.js +33 -2
  110. package/dist/powql.d.ts +61 -9
  111. package/dist/powql.js +187 -50
  112. package/dist/prisma-compat.js +160 -41
  113. package/dist/query/aggregates.d.ts +1 -1
  114. package/dist/query/aggregates.js +82 -20
  115. package/dist/query/batched-loader.d.ts +10 -0
  116. package/dist/query/batched-loader.js +270 -9
  117. package/dist/query/builder.d.ts +73 -0
  118. package/dist/query/builder.js +226 -30
  119. package/dist/query/filters.d.ts +162 -0
  120. package/dist/query/filters.js +246 -1
  121. package/dist/query/relations.d.ts +10 -10
  122. package/dist/query/relations.js +94 -14
  123. package/dist/query/types.d.ts +14 -1
  124. package/dist/query/utils.d.ts +146 -2
  125. package/dist/query/utils.js +204 -3
  126. package/dist/query/warn-registry.d.ts +10 -0
  127. package/dist/query/warn-registry.js +10 -0
  128. package/dist/query/where-compile.d.ts +30 -0
  129. package/dist/query/where-compile.js +40 -1
  130. package/dist/query/where.d.ts +128 -13
  131. package/dist/query/where.js +216 -80
  132. package/dist/query/writes.d.ts +1 -1
  133. package/dist/query/writes.js +40 -16
  134. package/dist/schema-builder.d.ts +2 -1
  135. package/dist/schema-sql.d.ts +94 -4
  136. package/dist/schema-sql.js +505 -30
  137. package/dist/schema.d.ts +3 -1
  138. package/dist/sqlite.d.ts +6 -0
  139. package/dist/sqlite.js +151 -10
  140. package/dist/typed-sql.d.ts +29 -1
  141. package/dist/typed-sql.js +30 -12
  142. package/package.json +6 -4
@@ -19,6 +19,7 @@ exports.diffReferentialAction = diffReferentialAction;
19
19
  exports.diffEnumValues = diffEnumValues;
20
20
  exports.diffCheckConstraints = diffCheckConstraints;
21
21
  exports.schemaDiff = schemaDiff;
22
+ exports.planTypeChange = planTypeChange;
22
23
  exports.findDestructivePushStatements = findDestructivePushStatements;
23
24
  exports.schemaPush = schemaPush;
24
25
  exports.schemaToSQLString = schemaToSQLString;
@@ -588,22 +589,49 @@ function diffCheckConstraints(table, schemaChecks, dbChecks, dialect = dialect_j
588
589
  /**
589
590
  * Compare a SchemaDef against a live Postgres database and return the diff.
590
591
  *
591
- * Connects to the database, inspects the public schema, and computes what
592
- * DDL is needed to make the database match the schema definition.
592
+ * Connects to the database, inspects `options.schema` (default `public`), and
593
+ * computes what DDL is needed to make the database match the schema definition.
593
594
  */
594
- async function schemaDiff(schema, connectionString) {
595
+ async function schemaDiff(schema, connectionString, options = {}) {
595
596
  const dialect = dialect_js_1.postgresDialect;
597
+ // The Postgres namespace every catalog read below is scoped to. Named
598
+ // `pgSchema` because `schema` is already the SchemaDef in this function.
599
+ const pgSchema = options.schema ?? 'public';
596
600
  const client = new pg_1.default.Client({ connectionString });
597
601
  await client.connect();
598
602
  try {
599
- // Get existing tables in the public schema
600
- const tableResult = await client.query(`SELECT tablename FROM pg_tables WHERE schemaname = 'public'`);
603
+ // Get existing tables in the target schema
604
+ const tableResult = await client.query(`SELECT tablename FROM pg_tables WHERE schemaname = $1`, [pgSchema]);
601
605
  const existingTables = new Set(tableResult.rows.map((r) => r.tablename));
602
- // Get existing columns for all tables
603
- const columnResult = await client.query(`SELECT table_name, column_name, data_type, udt_name, is_nullable, column_default, character_maximum_length
606
+ // Get existing columns for all tables. `numeric_precision` / `numeric_scale`
607
+ // ride along with `character_maximum_length` so a length- or precision-only
608
+ // change is visible (see typeDifference); comparing `udt_name` alone
609
+ // reported "already in sync" for VARCHAR(255) against a declared
610
+ // varchar(10), and the widening direction of that produced runtime
611
+ // "value too long" errors the diff had said were impossible.
612
+ const columnResult = await client.query(`SELECT table_name, column_name, data_type, udt_name, is_nullable, column_default,
613
+ character_maximum_length, numeric_precision, numeric_scale
604
614
  FROM information_schema.columns
605
- WHERE table_schema = 'public'
606
- ORDER BY table_name, ordinal_position`);
615
+ WHERE table_schema = $1
616
+ ORDER BY table_name, ordinal_position`, [pgSchema]);
617
+ // The type EXACTLY as Postgres writes it, for the modifiers
618
+ // information_schema structurally cannot report: an array's element length
619
+ // (`character varying(255)[]`, where character_maximum_length is NULL) and a
620
+ // pgvector dimension count (`vector(3)`, which has no column at all). Both
621
+ // were invisible to the diff, so both reported "already in sync" for a
622
+ // schema that differed. It doubles as the reverse-ALTER spelling of any type
623
+ // this generator has no keyword for. Restricted to ordinary and partitioned
624
+ // tables to match the pg_tables read above.
625
+ const formattedResult = await client.query(`SELECT cl.relname AS table_name, a.attname AS column_name,
626
+ format_type(a.atttypid, a.atttypmod) AS formatted_type
627
+ FROM pg_attribute a
628
+ JOIN pg_class cl ON cl.oid = a.attrelid
629
+ JOIN pg_namespace n ON n.oid = cl.relnamespace
630
+ WHERE n.nspname = $1 AND cl.relkind IN ('r', 'p') AND a.attnum > 0 AND NOT a.attisdropped`, [pgSchema]);
631
+ const formattedTypes = new Map();
632
+ for (const row of formattedResult.rows) {
633
+ formattedTypes.set(`${row.table_name}.${row.column_name}`, row.formatted_type);
634
+ }
607
635
  const dbColumns = {};
608
636
  for (const row of columnResult.rows) {
609
637
  if (!dbColumns[row.table_name]) {
@@ -615,6 +643,9 @@ async function schemaDiff(schema, connectionString) {
615
643
  isNullable: row.is_nullable === 'YES',
616
644
  columnDefault: row.column_default,
617
645
  maxLength: row.character_maximum_length,
646
+ numericPrecision: row.numeric_precision,
647
+ numericScale: row.numeric_scale,
648
+ formattedType: formattedTypes.get(`${row.table_name}.${row.column_name}`) ?? null,
618
649
  };
619
650
  }
620
651
  // Get single-column UNIQUE constraints (excluding PKs)
@@ -623,15 +654,15 @@ async function schemaDiff(schema, connectionString) {
623
654
  JOIN information_schema.key_column_usage kcu
624
655
  ON tc.constraint_name = kcu.constraint_name
625
656
  AND tc.table_schema = kcu.table_schema
626
- WHERE tc.table_schema = 'public'
657
+ WHERE tc.table_schema = $1
627
658
  AND tc.constraint_type = 'UNIQUE'
628
659
  AND tc.constraint_name IN (
629
660
  SELECT constraint_name
630
661
  FROM information_schema.key_column_usage
631
- WHERE table_schema = 'public'
662
+ WHERE table_schema = $1
632
663
  GROUP BY constraint_name
633
664
  HAVING COUNT(*) = 1
634
- )`);
665
+ )`, [pgSchema]);
635
666
  // Map: table → column → constraint_name for single-col uniques
636
667
  const dbUniques = {};
637
668
  for (const row of uniqueResult.rows) {
@@ -644,8 +675,8 @@ async function schemaDiff(schema, connectionString) {
644
675
  FROM pg_type t
645
676
  JOIN pg_enum e ON t.oid = e.enumtypid
646
677
  JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
647
- WHERE n.nspname = 'public'
648
- ORDER BY t.typname, e.enumsortorder`);
678
+ WHERE n.nspname = $1
679
+ ORDER BY t.typname, e.enumsortorder`, [pgSchema]);
649
680
  const dbEnums = {};
650
681
  for (const row of enumResult.rows) {
651
682
  if (!dbEnums[row.typname])
@@ -662,8 +693,8 @@ async function schemaDiff(schema, connectionString) {
662
693
  JOIN pg_catalog.pg_class tgt ON tgt.oid = con.confrelid
663
694
  JOIN pg_catalog.pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = con.conkey[1]
664
695
  JOIN pg_catalog.pg_attribute tatt ON tatt.attrelid = con.confrelid AND tatt.attnum = con.confkey[1]
665
- WHERE con.contype = 'f' AND n.nspname = 'public'
666
- AND array_length(con.conkey, 1) = 1`);
696
+ WHERE con.contype = 'f' AND n.nspname = $1
697
+ AND array_length(con.conkey, 1) = 1`, [pgSchema]);
667
698
  const dbForeignKeys = {};
668
699
  for (const row of fkResult.rows) {
669
700
  if (!dbForeignKeys[row.table_name])
@@ -682,7 +713,7 @@ async function schemaDiff(schema, connectionString) {
682
713
  FROM pg_constraint con
683
714
  JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid
684
715
  JOIN pg_catalog.pg_namespace n ON n.oid = con.connamespace
685
- WHERE con.contype = 'c' AND n.nspname = 'public'`);
716
+ WHERE con.contype = 'c' AND n.nspname = $1`, [pgSchema]);
686
717
  const dbChecks = {};
687
718
  for (const row of checkResult.rows) {
688
719
  if (!dbChecks[row.table_name])
@@ -692,7 +723,7 @@ async function schemaDiff(schema, connectionString) {
692
723
  // Existing index NAMES per table (for the declared-index diff). Covers PK,
693
724
  // unique-constraint, FK, and user indexes alike; the diff only ADDs declared
694
725
  // indexes whose name is missing and never auto-drops any (see below).
695
- const indexResult = await client.query(`SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = 'public'`);
726
+ const indexResult = await client.query(`SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = $1`, [pgSchema]);
696
727
  const dbIndexes = {};
697
728
  for (const row of indexResult.rows) {
698
729
  if (!dbIndexes[row.tablename])
@@ -771,17 +802,48 @@ async function schemaDiff(schema, connectionString) {
771
802
  // (a downcast on a PK loses data / breaks the sequence). This also
772
803
  // preserves back-compat for DBs whose `serial` columns were created as
773
804
  // BIGSERIAL (int8) before 0.24.0, `push` won't try to shrink them.
774
- const expectedUdt = schemaTypeToUdt(config);
775
- if (expectedUdt && !isSerialType(config.type) && dbCol.udtName !== expectedUdt) {
805
+ // planTypeChange owns the whole decision: whether the types differ at
806
+ // all (including length / precision modifiers), whether the conversion
807
+ // needs an explicit USING, and what the conversion costs.
808
+ const typePlan = planTypeChange(config, dbCol, snakeName);
809
+ if (typePlan.kind === 'warn') {
810
+ result.warnings.push(`column "${tableName}"."${snakeName}": ${typePlan.reason}`);
811
+ }
812
+ else if (typePlan.kind === 'alter') {
776
813
  // resolveDdlType handles enum names, vector(n), arrays, and VARCHAR(n) -
777
814
  // config.type alone would emit the internal ENUM/VECTOR sentinels here.
778
815
  const sqlType = resolveDdlType(config, dialect, snakeName);
779
- const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
780
- const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
781
- const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${oldSqlType} USING ${dialect.quoteIdentifier(snakeName)}::${oldSqlType};`;
816
+ const oldSqlType = udtToSqlType(dbCol);
817
+ const col = dialect.quoteIdentifier(snakeName);
818
+ // `USING` only where the conversion needs one. Within a type family
819
+ // Postgres has an assignment cast, and letting IT decide is the whole
820
+ // point: an explicit `::VARCHAR(10)` truncates a 27-character value,
821
+ // while the plain ALTER raises "value too long" and the migration
822
+ // stops. See planTypeChange.
823
+ //
824
+ // Where a USING IS needed (the conversion crosses type families and
825
+ // has no assignment cast), the cast targets the UNBOUNDED base type
826
+ // rather than the declared one, because an explicit cast amputates
827
+ // wherever it lands. Dropping the USING was only half the fix: every
828
+ // cross-family conversion INTO a bounded string still carried
829
+ // `USING col::VARCHAR(n)` and still truncated, silently and on every
830
+ // engine-side value shape (jsonb, integer, uuid, an enum label, bytea,
831
+ // a timestamp). Casting to the unbounded base keeps the conversion
832
+ // Postgres cannot do implicitly while leaving the LENGTH check to the
833
+ // assignment into the column, which raises "value too long" and stops.
834
+ const using = typePlan.needsUsing ? ` USING ${col}::${unboundedCastType(sqlType)}` : '';
835
+ const reverseUsing = typePlan.needsUsing ? ` USING ${col}::${unboundedCastType(oldSqlType)}` : '';
836
+ const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${col} TYPE ${sqlType}${using};`;
837
+ const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${col} TYPE ${oldSqlType}${reverseUsing};`;
782
838
  alterDef.columns.push({ column: snakeName, action: 'alter_type', sql, reverseSql });
783
839
  result.statements.push(sql);
784
840
  result.reverseStatements.unshift(reverseSql);
841
+ // The destructive gate sees the statement and refuses by default; its
842
+ // label is necessarily generic ("cast may truncate or fail"). This
843
+ // warning names what THIS conversion does to THESE rows.
844
+ if (typePlan.loss) {
845
+ result.warnings.push(`column "${tableName}"."${snakeName}" changes type from ${oldSqlType} to ${sqlType}: ${typePlan.loss}`);
846
+ }
785
847
  }
786
848
  // Check NOT NULL mismatch
787
849
  const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || isSerialType(config.type);
@@ -995,6 +1057,294 @@ async function schemaDiff(schema, connectionString) {
995
1057
  await client.end();
996
1058
  }
997
1059
  }
1060
+ /**
1061
+ * Rough type families. Within a family Postgres has an ASSIGNMENT cast between
1062
+ * every pair the schema builder can produce, which is what lets the diff emit a
1063
+ * plain `ALTER ... TYPE` with no `USING` (see {@link planTypeChange}). That
1064
+ * sentence is the family's whole contract, and two things used to break it:
1065
+ *
1066
+ * - ARRAYS returned null, so every array-to-array change was treated as a
1067
+ * cross-family conversion and got an explicit `USING col::VARCHAR(5)[]`,
1068
+ * which truncates each element exactly as the scalar form does. Postgres
1069
+ * has the same assignment casts between array types as between their
1070
+ * elements (verified pair by pair for every type the schema builder can
1071
+ * produce), so an array's family is its ELEMENT's family with `[]`
1072
+ * appended. The suffix matters: `text` and `varchar(5)[]` must NOT come out
1073
+ * the same family, because there is no automatic cast between a scalar and
1074
+ * an array and the plain ALTER would be unrunnable.
1075
+ * - `time` / `timetz` were listed as `temporal` alongside date and the
1076
+ * timestamps, and the contract simply is not true there: pg_cast has NO
1077
+ * entry between `time` and `date`, `timestamp` or `timestamptz` in EITHER
1078
+ * direction, so `ALTER COLUMN c TYPE DATE` on a `time` column fails with
1079
+ * "cannot be cast automatically". They are their own family, which makes a
1080
+ * time-to-date change cross-family: still not something Turbine can apply,
1081
+ * but now it says so in the loss warning instead of asserting a cast that
1082
+ * does not exist. (`{date, timestamp, timestamptz}` and `{time, timetz}`
1083
+ * are each internally complete.)
1084
+ */
1085
+ function udtFamily(udt) {
1086
+ if (udt.startsWith('_')) {
1087
+ const element = udtFamily(udt.slice(1));
1088
+ return element === null ? null : `${element}[]`;
1089
+ }
1090
+ switch (udt) {
1091
+ case 'text':
1092
+ case 'varchar':
1093
+ case 'bpchar':
1094
+ case 'char':
1095
+ return 'string';
1096
+ case 'int2':
1097
+ case 'int4':
1098
+ case 'int8':
1099
+ case 'numeric':
1100
+ case 'float4':
1101
+ case 'float8':
1102
+ return 'numeric';
1103
+ case 'date':
1104
+ case 'timestamp':
1105
+ case 'timestamptz':
1106
+ return 'temporal';
1107
+ case 'time':
1108
+ case 'timetz':
1109
+ return 'time-of-day';
1110
+ case 'json':
1111
+ case 'jsonb':
1112
+ return 'json';
1113
+ case 'bool':
1114
+ return 'boolean';
1115
+ default:
1116
+ return null; // uuid, bytea, enums, vector: no intra-family conversions
1117
+ }
1118
+ }
1119
+ /** Drop the array marker from a family name, so the per-family rules apply to both. */
1120
+ function elementFamily(family) {
1121
+ if (family === null)
1122
+ return null;
1123
+ return family.endsWith('[]') ? family.slice(0, -2) : family;
1124
+ }
1125
+ /** Drop Postgres's array prefix from a udt name (`_int4` → `int4`). */
1126
+ function elementUdt(udt) {
1127
+ return udt.startsWith('_') ? udt.slice(1) : udt;
1128
+ }
1129
+ /** Render a udt name the way a reader writes it (`_varchar` → `varchar[]`). */
1130
+ function describeUdt(udt) {
1131
+ return udt.startsWith('_') ? `${udt.slice(1)}[]` : udt;
1132
+ }
1133
+ /**
1134
+ * The type a `USING` cast should name so the cast itself cannot lose data.
1135
+ *
1136
+ * Only `VARCHAR(n)` (and its array form) carries a bound that an explicit cast
1137
+ * silently enforces by TRUNCATING; `TEXT` is the same type family with no bound,
1138
+ * so `col::TEXT` converts exactly as `col::VARCHAR(n)` would and then the
1139
+ * assignment into the declared column applies the length check properly. Every
1140
+ * other type this generator emits either has no length modifier or refuses the
1141
+ * conversion outright (pgvector's dimension count), so it is returned unchanged.
1142
+ */
1143
+ function unboundedCastType(sqlType) {
1144
+ const bounded = /^VARCHAR\(\d+\)(\[\])?$/i.exec(sqlType);
1145
+ return bounded ? `TEXT${bounded[1] ?? ''}` : sqlType;
1146
+ }
1147
+ /** Rank within the integer/real widening order, for the narrowing checks. */
1148
+ const NUMERIC_RANK = { int2: 1, int4: 2, int8: 3, float4: 4, float8: 5, numeric: 6 };
1149
+ /**
1150
+ * Rank within the temporal detail order (a lower rank carries less
1151
+ * information). Covers the `temporal` family only: `time` / `timetz` are a
1152
+ * separate family with no cast to or from these, so ranking them against a
1153
+ * date would compare two values that never meet.
1154
+ */
1155
+ const TEMPORAL_RANK = { date: 1, timestamp: 3, timestamptz: 4 };
1156
+ /**
1157
+ * The single numeric modifier in a `format_type` rendering, or null when the
1158
+ * type carries none. `character varying(255)[]` → 255, `vector(3)` → 3,
1159
+ * `numeric(10,2)` → 10, `text` → null.
1160
+ *
1161
+ * This exists because information_schema cannot express these: it reports
1162
+ * `character_maximum_length` NULL for an ARRAY of varchar (the length belongs to
1163
+ * the element type) and has no column at all for a pgvector dimension count. A
1164
+ * diff reading it alone therefore saw `varchar(255)[]` and a declared
1165
+ * `varchar(5)[]` as identical, and `vector(3)` and a declared 1536 dimensions as
1166
+ * identical, and reported "already in sync" for both.
1167
+ */
1168
+ function typeModifier(formattedType) {
1169
+ if (!formattedType)
1170
+ return null;
1171
+ const match = /\((\d+)(?:\s*,\s*\d+)?\)/.exec(formattedType);
1172
+ return match ? Number(match[1]) : null;
1173
+ }
1174
+ /**
1175
+ * The length bound on a string column, from whichever source carries it:
1176
+ * information_schema for a scalar varchar, the `format_type` modifier for an
1177
+ * array of varchar. Null means unbounded (`text`, a bare `VARCHAR`), which for
1178
+ * the loss checks reads as "could be longer than any target bound".
1179
+ */
1180
+ function stringLengthOf(col) {
1181
+ return col.maxLength ?? typeModifier(col.formattedType);
1182
+ }
1183
+ /**
1184
+ * A sentence naming what this specific conversion does to existing rows, or
1185
+ * undefined when nothing is lost. Deliberately says whether Postgres REFUSES
1186
+ * the row or silently changes it: those are very different operational events,
1187
+ * and the generic "cast may truncate or fail" told the reader neither.
1188
+ */
1189
+ function describeTypeLoss(from, toUdt, toMaxLength, column) {
1190
+ const q = `"${column}"`;
1191
+ const fromFamily = udtFamily(from.udtName);
1192
+ const toFamily = udtFamily(toUdt);
1193
+ // Within one family the rules are the same for a scalar and for an array of
1194
+ // it; only the noun changes, since an array's bound applies per element.
1195
+ const sameFamily = fromFamily !== null && fromFamily === toFamily;
1196
+ const family = elementFamily(toFamily);
1197
+ const fromUdt = elementUdt(from.udtName);
1198
+ const toElementUdt = elementUdt(toUdt);
1199
+ const isArray = toFamily?.endsWith('[]') ?? false;
1200
+ const subject = isArray ? `array elements in ${q}` : `values in ${q}`;
1201
+ // A bounded string TARGET enforces its bound on every value, whatever the
1202
+ // source type was, because the conversion renders the value as text first.
1203
+ // Only the same-family case can prove the bound is already satisfied.
1204
+ if (family === 'string' && toMaxLength != null) {
1205
+ const fromLen = sameFamily ? stringLengthOf(from) : null;
1206
+ if (fromLen === null || fromLen > toMaxLength) {
1207
+ const rendered = sameFamily ? '' : 'whose text form is ';
1208
+ return `${subject} ${rendered}longer than ${toMaxLength} characters make this statement FAIL (Postgres refuses the conversion; it does not truncate). Shorten or migrate those rows first.`;
1209
+ }
1210
+ }
1211
+ if (family === 'numeric' && sameFamily) {
1212
+ const fromRank = NUMERIC_RANK[fromUdt] ?? 0;
1213
+ const toRank = NUMERIC_RANK[toElementUdt] ?? 0;
1214
+ const toIsInteger = toElementUdt === 'int2' || toElementUdt === 'int4' || toElementUdt === 'int8';
1215
+ const fromIsInteger = fromUdt === 'int2' || fromUdt === 'int4' || fromUdt === 'int8';
1216
+ if (toIsInteger && !fromIsInteger) {
1217
+ return `fractional ${subject} are ROUNDED to whole numbers (silently, row by row).`;
1218
+ }
1219
+ if (toIsInteger && fromIsInteger && toRank < fromRank) {
1220
+ return `${subject} outside the ${toElementUdt} range make this statement FAIL ("integer out of range").`;
1221
+ }
1222
+ if (toRank < fromRank) {
1223
+ return `${subject} lose precision (silently rounded to what ${toElementUdt} can represent).`;
1224
+ }
1225
+ }
1226
+ if (family === 'temporal' && sameFamily) {
1227
+ const fromRank = TEMPORAL_RANK[fromUdt] ?? 0;
1228
+ const toRank = TEMPORAL_RANK[toElementUdt] ?? 0;
1229
+ if (toElementUdt === 'date' && fromRank > 1)
1230
+ return `the time-of-day component of ${q} is DROPPED (silently).`;
1231
+ if (toRank < fromRank)
1232
+ return `${q} loses its time-zone offset (converted to the session time zone).`;
1233
+ }
1234
+ if (elementFamily(fromFamily) === 'time-of-day' && family === 'temporal') {
1235
+ // Not "fails for some rows": pg_cast has no entry between `time`/`timetz`
1236
+ // and `date`/`timestamp`/`timestamptz` in either direction, implicit,
1237
+ // assignment or explicit, so the statement cannot run at all, on an empty
1238
+ // table included. Saying "must be a valid date literal" would send the
1239
+ // reader off to clean data that was never the problem.
1240
+ return `Postgres has NO cast from ${describeUdt(from.udtName)} to ${describeUdt(toUdt)}, in either direction, so this statement ALWAYS FAILS (a time of day carries no date to convert). Add the new column and backfill it in a manual migration instead.`;
1241
+ }
1242
+ if (!sameFamily) {
1243
+ return `every existing value in ${q} must be a valid ${describeUdt(toUdt)} literal or this statement FAILS.`;
1244
+ }
1245
+ return '';
1246
+ }
1247
+ /**
1248
+ * Decide what the diff should emit for one column's type.
1249
+ *
1250
+ * Two things this deliberately does that the previous `udtName !== expectedUdt`
1251
+ * check did not:
1252
+ *
1253
+ * 1. It compares LENGTH and NUMERIC PRECISION, not just the UDT name. A
1254
+ * declared `varchar(10)` against a database `VARCHAR(255)` produced no
1255
+ * statement and no warning, so `push` printed "Database is already in
1256
+ * sync" for a schema that genuinely differed. The widening direction is
1257
+ * the operationally painful one: the schema says 255, the column is 10,
1258
+ * push says in sync, and writes fail at runtime with "value too long".
1259
+ * 2. It reports `needsUsing` FALSE for a same-family conversion. The old
1260
+ * statement always appended `USING col::type`, and an EXPLICIT cast to
1261
+ * `varchar(n)` TRUNCATES where the plain assignment cast raises "value too
1262
+ * long". Postgres refuses that migration on its own; the generated `USING`
1263
+ * converted the refusal into silent data amputation. Same shape for
1264
+ * `numeric -> integer` and `timestamptz -> date`. `USING` is emitted only
1265
+ * where the conversion genuinely needs it (text to uuid, text to an enum,
1266
+ * anything crossing a type family), where no assignment cast exists.
1267
+ *
1268
+ * Numeric PRECISION drift gets a warning rather than a statement: `defineSchema`
1269
+ * has no way to declare `numeric(10, 2)`, so every such column would otherwise
1270
+ * get a table-rewriting `ALTER ... TYPE NUMERIC` on every push, discarding a
1271
+ * constraint the schema never had the vocabulary to ask for.
1272
+ */
1273
+ function planTypeChange(config, dbCol, column) {
1274
+ const expectedUdt = schemaTypeToUdt(config);
1275
+ if (expectedUdt === null) {
1276
+ return {
1277
+ kind: 'warn',
1278
+ reason: `schema type "${config.type}" has no known Postgres type mapping, so its type is not diffed.`,
1279
+ };
1280
+ }
1281
+ if (isSerialType(config.type))
1282
+ return { kind: 'none' };
1283
+ if (dbCol.udtName !== expectedUdt) {
1284
+ const loss = describeTypeLoss(dbCol, expectedUdt, config.maxLength, column);
1285
+ const sameFamily = udtFamily(dbCol.udtName) !== null && udtFamily(dbCol.udtName) === udtFamily(expectedUdt);
1286
+ return { kind: 'alter', needsUsing: !sameFamily, ...(loss ? { loss } : {}) };
1287
+ }
1288
+ // Same UDT: the remaining differences are the type MODIFIERS. Some are in
1289
+ // information_schema, some only in the catalog's format_type rendering.
1290
+ if (expectedUdt === 'varchar') {
1291
+ // `maxLength: null` on a varchar column is a real declaration (unbounded
1292
+ // VARCHAR), not an omission: schemaToSQL emits a bare `VARCHAR` for it.
1293
+ const want = config.maxLength ?? null;
1294
+ if (want !== dbCol.maxLength) {
1295
+ const loss = describeTypeLoss(dbCol, expectedUdt, want, column);
1296
+ return { kind: 'alter', needsUsing: false, ...(loss ? { loss } : {}) };
1297
+ }
1298
+ return { kind: 'none' };
1299
+ }
1300
+ if (expectedUdt === '_varchar') {
1301
+ // The same length comparison for `varchar(n)[]`. It needs a different
1302
+ // source because information_schema reports character_maximum_length NULL
1303
+ // for an array (the modifier belongs to the element type), so comparing it
1304
+ // saw `varchar(255)[]` and a declared `varchar(5)[]` as identical and
1305
+ // reported "already in sync" for a schema that genuinely differed, with no
1306
+ // statement and no warning. The plain ALTER carries the change on this path
1307
+ // too: Postgres has the same assignment cast between the array types as
1308
+ // between their elements, and enforces the bound per element.
1309
+ const want = config.maxLength ?? null;
1310
+ if (want !== typeModifier(dbCol.formattedType)) {
1311
+ const loss = describeTypeLoss(dbCol, expectedUdt, want, column);
1312
+ return { kind: 'alter', needsUsing: false, ...(loss ? { loss } : {}) };
1313
+ }
1314
+ return { kind: 'none' };
1315
+ }
1316
+ if (expectedUdt === 'vector') {
1317
+ // pgvector's dimension count is a type modifier, invisible to
1318
+ // information_schema, so a `vector(3)` column against a declared 1536
1319
+ // dimensions produced no statement and no warning; the app then failed on
1320
+ // its first write with "expected 3 dimensions, not 1536". Same shape as the
1321
+ // varchar(255)-against-varchar(10) bug above. A dimension change cannot be
1322
+ // applied to populated rows at all (pgvector rejects it with or without an
1323
+ // explicit cast), which is what the loss sentence has to say.
1324
+ const want = config.vectorDimensions ?? null;
1325
+ const have = typeModifier(dbCol.formattedType);
1326
+ if (want !== have) {
1327
+ const loss = want == null
1328
+ ? `existing vectors in "${column}" keep their dimension count; an unbounded vector column cannot be produced from a bounded one while rows exist, so this statement FAILS.`
1329
+ : `every existing vector in "${column}" must already have exactly ${want} dimensions or this statement FAILS (pgvector refuses the conversion; it does not pad or truncate).`;
1330
+ return { kind: 'alter', needsUsing: false, loss };
1331
+ }
1332
+ return { kind: 'none' };
1333
+ }
1334
+ if (expectedUdt === 'numeric' && dbCol.numericPrecision !== null) {
1335
+ // Guarded on `numeric` alone: information_schema reports a precision for
1336
+ // every integer and float type too (int4 -> 32), which is not a modifier
1337
+ // anyone declared.
1338
+ const stored = dbCol.numericScale === null
1339
+ ? `numeric(${dbCol.numericPrecision})`
1340
+ : `numeric(${dbCol.numericPrecision}, ${dbCol.numericScale})`;
1341
+ return {
1342
+ kind: 'warn',
1343
+ reason: `the database column is ${stored} but the schema declares a bare NUMERIC. defineSchema cannot express numeric precision/scale, so Turbine leaves the column alone rather than rewriting away a constraint it cannot represent. Change it in a manual migration if that is intended.`,
1344
+ };
1345
+ }
1346
+ return { kind: 'none' };
1347
+ }
998
1348
  /**
999
1349
  * Map a schema column type to its expected PostgreSQL UDT name.
1000
1350
  */
@@ -1029,15 +1379,35 @@ function schemaTypeToUdt(config) {
1029
1379
  return base;
1030
1380
  }
1031
1381
  /**
1032
- * Reverse map: PostgreSQL UDT name → SQL type (for generating reverse ALTER TYPE).
1382
+ * Reverse map: a database column's type the SQL type token that restores it
1383
+ * (used for the reverse ALTER TYPE, and to name the old type in a warning).
1384
+ *
1385
+ * Two rules, and the second one is why this takes the whole column:
1386
+ *
1387
+ * 1. A type this generator itself emits gets its own uppercase spelling back,
1388
+ * so a reverse statement reads like the forward one. `numeric` now carries
1389
+ * its precision and scale: a `numeric(10, 2)` column changed to INTEGER
1390
+ * reversed to a bare `NUMERIC`, quietly discarding the constraint the DOWN
1391
+ * was supposed to restore.
1392
+ * 2. ANYTHING ELSE is not a keyword and must not be treated as one. The old
1393
+ * fallback was `udtName.toUpperCase()`, which produces an unquoted
1394
+ * identifier: correct only by accident for an all-lowercase name, and
1395
+ * wrong for every enum, domain or extension type with a capital in it
1396
+ * (`"Probe"` became `PROBE`, which Postgres folds back to `probe` and then
1397
+ * fails with `type "probe" does not exist`). The catalog's own
1398
+ * `format_type` rendering is the answer: already quoted where quoting is
1399
+ * needed, already schema-qualified when the type is not in the search_path,
1400
+ * and already carrying its modifiers (`vector(3)`, `citext`,
1401
+ * `character varying(255)[]`). Quoting the bare udt name is the fallback
1402
+ * for a hand-built column with no catalog rendering.
1033
1403
  */
1034
- function udtToSqlType(udtName, maxLength) {
1404
+ function udtToSqlType(dbCol) {
1035
1405
  const map = {
1036
1406
  int8: 'BIGINT',
1037
1407
  int4: 'INTEGER',
1038
1408
  int2: 'SMALLINT',
1039
1409
  text: 'TEXT',
1040
- varchar: maxLength ? `VARCHAR(${maxLength})` : 'VARCHAR',
1410
+ varchar: dbCol.maxLength ? `VARCHAR(${dbCol.maxLength})` : 'VARCHAR',
1041
1411
  bool: 'BOOLEAN',
1042
1412
  timestamptz: 'TIMESTAMPTZ',
1043
1413
  date: 'DATE',
@@ -1045,10 +1415,21 @@ function udtToSqlType(udtName, maxLength) {
1045
1415
  uuid: 'UUID',
1046
1416
  float4: 'REAL',
1047
1417
  float8: 'DOUBLE PRECISION',
1048
- numeric: 'NUMERIC',
1418
+ numeric: numericSqlType(dbCol),
1049
1419
  bytea: 'BYTEA',
1050
1420
  };
1051
- return map[udtName] ?? udtName.toUpperCase();
1421
+ const mapped = map[dbCol.udtName];
1422
+ if (mapped)
1423
+ return mapped;
1424
+ return dbCol.formattedType ?? dialect_js_1.postgresDialect.quoteIdentifier(dbCol.udtName);
1425
+ }
1426
+ /** `NUMERIC`, `NUMERIC(10)` or `NUMERIC(10, 2)`, whichever the column actually is. */
1427
+ function numericSqlType(dbCol) {
1428
+ if (dbCol.numericPrecision == null)
1429
+ return 'NUMERIC';
1430
+ if (dbCol.numericScale == null)
1431
+ return `NUMERIC(${dbCol.numericPrecision})`;
1432
+ return `NUMERIC(${dbCol.numericPrecision}, ${dbCol.numericScale})`;
1052
1433
  }
1053
1434
  /**
1054
1435
  * Normalize a database default value for comparison.
@@ -1093,6 +1474,73 @@ function findDestructivePushStatements(statements) {
1093
1474
  hits.push(...(0, destructive_js_1.scanDestructiveSql)(stmt));
1094
1475
  return hits;
1095
1476
  }
1477
+ /**
1478
+ * Quote a schema name for use INSIDE a `search_path` GUC value. The value is
1479
+ * bound as a parameter to set_config, but Postgres parses its contents as an
1480
+ * identifier list, so a name needing quotes (mixed case, a dot, a space) has to
1481
+ * carry them itself. Doubling `"` follows the same rule as quoteIdent.
1482
+ */
1483
+ function quoteSearchPathIdent(name) {
1484
+ return `"${name.replace(/"/g, '""')}"`;
1485
+ }
1486
+ /**
1487
+ * Pin the transaction's `search_path` so the unqualified identifiers in the
1488
+ * generated DDL resolve in the SAME namespace the diff read, and refuse a
1489
+ * target schema that does not exist.
1490
+ *
1491
+ * Three separate failures live here, which is why it is one function:
1492
+ *
1493
+ * 1. The pin used to be applied only for a non-public target, on the reasoning
1494
+ * that the default path is already public. It is not: `search_path` is a
1495
+ * role/database/connection-string setting, so a role with
1496
+ * `search_path = app, public` and NO configured schema had the diff reading
1497
+ * `public` (unconditional, it binds the name as a parameter) while the DDL
1498
+ * landed in `app`. The push reports success, and the second push fails with
1499
+ * `relation "..." already exists` forever after, because the diff still
1500
+ * cannot see the table it created. Skipping the pin for `public` protected
1501
+ * the byte-identity of the SQL TEXT, which is not the thing that has to
1502
+ * stay identical; the EFFECT is, and the effect of pinning `"public"` on a
1503
+ * connection that already resolved to public is nil.
1504
+ * 2. Postgres does not validate schema names in `search_path`: setting it to a
1505
+ * namespace that does not exist succeeds, and name resolution simply skips
1506
+ * the missing entry. So a typo in `schema` silently redirected every
1507
+ * CREATE into whatever came next in the path (public), reported success,
1508
+ * and wedged the second push exactly as (1) does. `to_regnamespace`
1509
+ * returns NULL for a missing namespace, which is the check.
1510
+ * 3. Pinning `"<target>", public` REPLACED the caller's path rather than
1511
+ * extending it, so an extension installed in its own schema stopped
1512
+ * resolving. On the common managed-Postgres layout (`"$user", public,
1513
+ * extensions`) a `vector` / `citext` / `hstore` / `postgis` column, an
1514
+ * extension opclass in a CREATE INDEX, or a CHECK calling an extension
1515
+ * function all failed with `type "..." does not exist` under a pin that the
1516
+ * same DDL did not need without it. The caller's entries are appended
1517
+ * verbatim (they are already valid GUC syntax, and `SHOW` renders them with
1518
+ * whatever quoting they need), so the only change to resolution is that the
1519
+ * target schema is consulted FIRST, which is the whole point: an
1520
+ * unqualified CREATE uses the first entry.
1521
+ *
1522
+ * `set_config(..., true)` is transaction-local, so none of this can leak onto a
1523
+ * pooled backend, and the value is BOUND rather than interpolated.
1524
+ */
1525
+ async function pinSearchPath(client, pgSchema) {
1526
+ const present = await client.query(`SELECT to_regnamespace($1) IS NOT NULL AS present`, [
1527
+ pgSchema,
1528
+ ]);
1529
+ if (!present.rows[0]?.present) {
1530
+ throw new errors_js_1.ValidationError(`[turbine] Schema "${pgSchema}" does not exist in this database. ` +
1531
+ `Postgres accepts a missing namespace in search_path without complaint, so pushing anyway would ` +
1532
+ `create every table in whichever schema resolves next (usually public) and report success. ` +
1533
+ `Create the schema first (CREATE SCHEMA "${pgSchema}") or correct the configured schema name.`);
1534
+ }
1535
+ // Read the path the connection would otherwise use, so the pin EXTENDS it
1536
+ // instead of discarding it. An empty value is possible (`SHOW` renders it as
1537
+ // an empty identifier) and would leave a trailing comma, so it is dropped.
1538
+ const shown = await client.query('SHOW search_path');
1539
+ const inherited = (shown.rows[0]?.search_path ?? '').trim();
1540
+ const target = quoteSearchPathIdent(pgSchema);
1541
+ const value = inherited ? `${target}, ${inherited}` : target;
1542
+ await client.query(`SELECT set_config('search_path', $1, true)`, [value]);
1543
+ }
1096
1544
  /** Format the destructive-push refusal message (mirrors the migrate gate copy). */
1097
1545
  function formatDestructivePushError(hits) {
1098
1546
  const lines = ['[turbine] Refusing to apply schema changes containing DESTRUCTIVE statements:', ''];
@@ -1134,13 +1582,24 @@ exports.DestructivePushRefusal = DestructivePushRefusal;
1134
1582
  * `allowDestructive: true` is passed. The CLI (`turbine push`) catches this and
1135
1583
  * prompts for the same typed confirmation as `migrate up`; programmatic callers
1136
1584
  * must opt in explicitly.
1585
+ *
1586
+ * `options.schema` (default `public`) scopes BOTH halves: the diff reads that
1587
+ * namespace, and the generated DDL (which names tables unqualified) is executed
1588
+ * with a transaction-local `search_path` pinned so the target schema resolves
1589
+ * FIRST. Without the second half a diff that correctly found nothing in `app`
1590
+ * would still emit `CREATE TABLE "users"` into whatever the connection's
1591
+ * search_path happens to resolve to. The pin is unconditional (`public` is a
1592
+ * target like any other, since a role's own search_path may not lead there),
1593
+ * refuses a schema that does not exist, and APPENDS the connection's existing
1594
+ * path so types living elsewhere still resolve. See {@link pinSearchPath}.
1137
1595
  */
1138
1596
  async function schemaPush(schema, connectionString, options = {}) {
1597
+ const pgSchema = options.schema ?? 'public';
1139
1598
  // Accept a precomputed diff so a caller (the CLI) can diff ONCE, show the
1140
1599
  // plan, confirm, and apply the EXACT statements it displayed. Without this,
1141
1600
  // schemaPush would re-diff on the post-confirmation retry, so a concurrent
1142
1601
  // schema change between confirm and apply could alter the applied set (TOCTOU).
1143
- const diff = options.precomputedDiff ?? (await schemaDiff(schema, connectionString));
1602
+ const diff = options.precomputedDiff ?? (await schemaDiff(schema, connectionString, { schema: pgSchema }));
1144
1603
  const result = {
1145
1604
  statementsExecuted: 0,
1146
1605
  statements: diff.statements,
@@ -1162,6 +1621,10 @@ async function schemaPush(schema, connectionString, options = {}) {
1162
1621
  await client.connect();
1163
1622
  try {
1164
1623
  await client.query('BEGIN');
1624
+ // Unconditional: the diff's read side is unconditional too, and the two
1625
+ // halves disagreeing about which namespace they mean is what wedges a
1626
+ // project permanently. See pinSearchPath for all three failure modes.
1627
+ await pinSearchPath(client, pgSchema);
1165
1628
  for (const sql of diff.statements) {
1166
1629
  await client.query(sql);
1167
1630
  result.statementsExecuted++;
@@ -1169,7 +1632,20 @@ async function schemaPush(schema, connectionString, options = {}) {
1169
1632
  await client.query('COMMIT');
1170
1633
  }
1171
1634
  catch (err) {
1172
- await client.query('ROLLBACK');
1635
+ // The ROLLBACK is best-effort, and its own failure must never REPLACE the
1636
+ // error being unwound. The common way to get here is a connection that has
1637
+ // already died: the DDL fails, the ROLLBACK then fails too, and an
1638
+ // unguarded `await` throws the connection error out of this catch block, so
1639
+ // the caller is shown a dead socket instead of the constraint violation or
1640
+ // syntax error that actually caused the push to fail. There is also nothing
1641
+ // to do about a failed ROLLBACK: a broken connection is discarded below and
1642
+ // the server rolls the transaction back on its own.
1643
+ try {
1644
+ await client.query('ROLLBACK');
1645
+ }
1646
+ catch {
1647
+ // swallowed on purpose, see above
1648
+ }
1173
1649
  throw err;
1174
1650
  }
1175
1651
  finally {