turbine-orm 0.81.0 → 0.90.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 (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. package/skills/turbine-orm/SKILL.md +56 -6
@@ -9,17 +9,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9
9
  return (mod && mod.__esModule) ? mod : { "default": mod };
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.DestructivePushRefusal = void 0;
12
+ exports.DestructivePushRefusal = exports.NEVER_COMPARED = void 0;
13
13
  exports.referentialActionToSql = referentialActionToSql;
14
14
  exports.schemaToSQL = schemaToSQL;
15
15
  exports.describeIndexDefMismatch = describeIndexDefMismatch;
16
16
  exports.undeclaredIndexWarnings = undeclaredIndexWarnings;
17
+ exports.driftWarnings = driftWarnings;
17
18
  exports.buildAddForeignKeyStatement = buildAddForeignKeyStatement;
18
19
  exports.diffReferentialAction = diffReferentialAction;
19
20
  exports.diffEnumValues = diffEnumValues;
20
21
  exports.diffCheckConstraints = diffCheckConstraints;
21
22
  exports.schemaDiff = schemaDiff;
22
23
  exports.planTypeChange = planTypeChange;
24
+ exports.isLosslessWidening = isLosslessWidening;
23
25
  exports.findDestructivePushStatements = findDestructivePushStatements;
24
26
  exports.schemaPush = schemaPush;
25
27
  exports.schemaToSQLString = schemaToSQLString;
@@ -127,13 +129,18 @@ function schemaToSQL(schema, options) {
127
129
  : 'CREATE EXTENSION IF NOT EXISTS vector;');
128
130
  }
129
131
  // CREATE TYPE for every schema-level enum, before the tables that use them.
130
- for (const [enumName, labels] of Object.entries(schema.enums ?? {})) {
131
- statements.push(generateCreateEnumType(enumName, labels, dialect));
132
+ // Only where enums are named types (PostgreSQL): every other engine declares
133
+ // the labels on the column itself (see `enumColumnType`), and has no CREATE
134
+ // TYPE to run.
135
+ if (dialect.capabilities.namedEnumTypes) {
136
+ for (const [enumName, labels] of Object.entries(schema.enums ?? {})) {
137
+ statements.push(generateCreateEnumType(enumName, labels, dialect));
138
+ }
132
139
  }
133
140
  // Generate CREATE TABLE statements
134
141
  for (const tableName of sorted) {
135
142
  const table = schema.tables[tableName];
136
- statements.push(generateCreateTable(table, resolveRef, dialect));
143
+ statements.push(generateCreateTable(table, resolveRef, dialect, schema.enums));
137
144
  }
138
145
  // Generate CREATE INDEX for foreign key columns
139
146
  for (const tableName of sorted) {
@@ -239,13 +246,22 @@ function topologicalSort(schema) {
239
246
  * to their snake_case DDL form, so users can write either camelCase JS
240
247
  * accessor names or snake_case DDL names.
241
248
  */
242
- function generateCreateTable(table, resolveRef, dialect = dialect_js_1.postgresDialect) {
249
+ function generateCreateTable(table, resolveRef, dialect = dialect_js_1.postgresDialect, enums) {
243
250
  const tableName = table.name;
244
251
  const columnDefs = [];
245
252
  const compositePk = table.primaryKey && table.primaryKey.length > 0 ? table.primaryKey : null;
253
+ const keyed = keyedColumnNames(table);
254
+ const inline = dialect.capabilities.inlineForeignKeys;
255
+ const foreignKeys = [];
246
256
  for (const [fieldName, config] of Object.entries(table.columns)) {
247
- columnDefs.push(generateColumnDef(fieldName, config, resolveRef, dialect));
257
+ columnDefs.push(generateColumnDef(fieldName, config, resolveRef, dialect, keyed.has((0, schema_js_1.camelToSnake)(fieldName)), inline, enums));
258
+ if (!inline) {
259
+ const fk = generateForeignKeyConstraint(fieldName, config, resolveRef, dialect);
260
+ if (fk)
261
+ foreignKeys.push(fk);
262
+ }
248
263
  }
264
+ columnDefs.push(...foreignKeys);
249
265
  // Append a table-level PRIMARY KEY constraint when a composite PK is set.
250
266
  if (compositePk) {
251
267
  const cols = compositePk.map((c) => dialect.quoteIdentifier((0, schema_js_1.camelToSnake)(c)));
@@ -265,11 +281,80 @@ function generateCreateTable(table, resolveRef, dialect = dialect_js_1.postgresD
265
281
  definitions: columnDefs,
266
282
  });
267
283
  }
284
+ /**
285
+ * The snake_case names of the columns of `table` that sit in a key: a primary
286
+ * key (single or composite), a UNIQUE column, a foreign key (Turbine indexes
287
+ * those), or a member of a declared index.
288
+ */
289
+ function keyedColumnNames(table) {
290
+ const keyed = new Set();
291
+ for (const [fieldName, config] of Object.entries(table.columns)) {
292
+ if (config.isPrimaryKey || config.isUnique || config.referencesTarget)
293
+ keyed.add((0, schema_js_1.camelToSnake)(fieldName));
294
+ }
295
+ for (const pk of table.primaryKey ?? [])
296
+ keyed.add((0, schema_js_1.camelToSnake)(pk));
297
+ for (const idx of table.indexes ?? []) {
298
+ if ((0, schema_builder_js_1.isDocFieldIndexDef)(idx))
299
+ continue;
300
+ for (const col of idx.columns)
301
+ keyed.add((0, schema_js_1.camelToSnake)(col));
302
+ }
303
+ return keyed;
304
+ }
305
+ /** A table-level `FOREIGN KEY` clause for a column's `references`, or null. */
306
+ function generateForeignKeyConstraint(fieldName, config, resolveRef, dialect) {
307
+ if (!config.referencesTarget)
308
+ return null;
309
+ const refParts = config.referencesTarget.split('.');
310
+ if (refParts.length !== 2)
311
+ return null;
312
+ const refTable = resolveRef ? resolveRef(refParts[0]) : refParts[0];
313
+ let clause = `FOREIGN KEY (${dialect.quoteIdentifier((0, schema_js_1.camelToSnake)(fieldName))}) REFERENCES ${dialect.quoteIdentifier(refTable)}(${dialect.quoteIdentifier(refParts[1])})`;
314
+ if (config.onDelete)
315
+ clause += ` ON DELETE ${referentialActionToSql(config.onDelete)}`;
316
+ if (config.onUpdate)
317
+ clause += ` ON UPDATE ${referentialActionToSql(config.onUpdate)}`;
318
+ return clause;
319
+ }
320
+ /**
321
+ * The type (and CHECK) of an enum column on an engine with no named enum type,
322
+ * from the dialect's `buildEnumColumn`, or null on PostgreSQL, where the column
323
+ * names the `CREATE TYPE`. The labels come from the schema's `enums`: off
324
+ * PostgreSQL there is no existing type a column could name instead.
325
+ */
326
+ function enumColumnType(config, dialect, column, enums) {
327
+ if (!config.enumName || dialect.capabilities.namedEnumTypes)
328
+ return null;
329
+ if (!dialect.buildEnumColumn) {
330
+ throw new errors_js_1.UnsupportedFeatureError('enum columns', dialect.name, `Column "${column}" is of enum type "${config.enumName}". Declare it as text with a CHECK constraint instead.`);
331
+ }
332
+ if (config.isArray) {
333
+ throw new errors_js_1.UnsupportedFeatureError('enum array columns', dialect.name, `Column "${column}" is an array of enum "${config.enumName}"; ${dialect.name} has no array columns.`);
334
+ }
335
+ const labels = enums?.[config.enumName];
336
+ if (!labels) {
337
+ throw new errors_js_1.ValidationError(`Column "${column}" is of enum type "${config.enumName}", which the schema does not declare. ` +
338
+ `${dialect.name} has no enum type to refer to, so the labels must be in the schema: ` +
339
+ `defineSchema(tables, { enums: { ${JSON.stringify(config.enumName)}: ['a', 'b'] } }).`);
340
+ }
341
+ if (labels.length === 0) {
342
+ throw new errors_js_1.ValidationError(`Enum "${config.enumName}" declares no labels, so column "${column}" could hold nothing.`);
343
+ }
344
+ return dialect.buildEnumColumn(dialect.quoteIdentifier(column), labels);
345
+ }
268
346
  /**
269
347
  * Generate a single column definition line (e.g. "id BIGSERIAL PRIMARY KEY").
348
+ *
349
+ * `keyed` marks a column that sits in a key (see {@link keyedColumnNames}); it
350
+ * only matters to dialects that cannot key an unbounded text type. `inlineReferences`
351
+ * is false for a dialect that declares foreign keys at table level instead.
352
+ * `enums` is the schema's enum declarations, needed off PostgreSQL to declare
353
+ * an enum column's labels on the column itself.
270
354
  */
271
- function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1.postgresDialect) {
355
+ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1.postgresDialect, keyed = false, inlineReferences = true, enums) {
272
356
  const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
357
+ const enumColumn = enumColumnType(config, dialect, snakeName, enums);
273
358
  // NOT NULL, serial types are implicitly NOT NULL, but explicit is fine.
274
359
  // A column is NOT NULL if:
275
360
  // 1. Explicitly marked .notNull(), OR
@@ -279,15 +364,13 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1
279
364
  const isSerial = isSerialType(config.type);
280
365
  const implicitNotNull = isSerial || config.isPrimaryKey;
281
366
  const notNull = config.isNotNull && !implicitNotNull;
282
- // DEFAULT
283
- let defaultValue;
284
- if (config.defaultValue != null) {
285
- defaultValue = normalizeDefault(config.defaultValue);
286
- }
367
+ // DEFAULT, normalized here and spelled for the dialect once the column's
368
+ // final type is known (a MySQL default depends on it).
369
+ const normalizedDefault = config.defaultValue != null ? normalizeDefault(config.defaultValue) : undefined;
287
370
  // REFERENCES, resolve the raw table name through the optional resolver so
288
371
  // both camelCase accessor names and snake_case DDL names work.
289
372
  let references;
290
- if (config.referencesTarget) {
373
+ if (config.referencesTarget && inlineReferences) {
291
374
  const refParts = config.referencesTarget.split('.');
292
375
  if (refParts.length === 2) {
293
376
  const rawTable = refParts[0];
@@ -298,14 +381,43 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1
298
381
  };
299
382
  }
300
383
  }
301
- // Resolve the DDL type (enum name / vector(n) / scalar, plus [] for arrays).
302
- // Passed as a fully-formed `type` token with no maxLength so the dialect
303
- // doesn't re-apply VARCHAR(n) on top of it.
304
- const ddlType = resolveDdlType(config, dialect, snakeName);
384
+ // The type is mapped ONCE, by the dialect's own `buildColumnDefinition`. A
385
+ // column that is a plain scalar hands it the schema-level token (`SERIAL`,
386
+ // `VARCHAR` + maxLength, `TEXT`), because every non-Postgres dialect keeps
387
+ // its auto-increment / identity marker, its length and its bounded string
388
+ // type INSIDE that mapping. Handing it the already-mapped token (`INT
389
+ // AUTO_INCREMENT`, `VARCHAR(12)`) made the dialect map the output of its own
390
+ // mapper, which turned `INT AUTO_INCREMENT` into `INT`, `VARCHAR(12)` into
391
+ // `VARCHAR(255)` and `NVARCHAR(MAX)` into `NVARCHAR(255)`. An enum name, a
392
+ // `vector(n)` and an array are Postgres tokens with no schema-level spelling,
393
+ // so they are passed through resolved.
394
+ const plainScalar = !config.enumName && config.vectorDimensions == null && !config.isArray;
395
+ let typeInput;
396
+ if (enumColumn) {
397
+ typeInput = { type: enumColumn.type, maxLength: enumColumn.maxLength ?? null };
398
+ }
399
+ else if (!plainScalar) {
400
+ typeInput = { type: resolveDdlType(config, dialect, snakeName), maxLength: null };
401
+ }
402
+ else if (keyed && config.type === 'TEXT' && !dialect.capabilities.unboundedTextKeys) {
403
+ // A keyed text / bytea column takes the dialect's bounded type
404
+ // (`VARCHAR(255)` / `NVARCHAR(255)`, `VARBINARY(255)`) where an unbounded
405
+ // one cannot be a key, or its own `maxLength` when it declares one.
406
+ typeInput = { type: 'VARCHAR', maxLength: config.maxLength };
407
+ }
408
+ else if (keyed && config.type === 'BYTEA' && !dialect.capabilities.unboundedTextKeys) {
409
+ typeInput = { type: 'VARBINARY', maxLength: config.maxLength };
410
+ }
411
+ else {
412
+ typeInput = { type: config.type, maxLength: config.maxLength };
413
+ }
414
+ const defaultValue = normalizedDefault === undefined
415
+ ? undefined
416
+ : dialectDefault(normalizedDefault, dialect, dialect.name === 'mysql' ? dialect.buildColumnType(typeInput) : '');
305
417
  let def = dialect.buildColumnDefinition({
306
418
  name: dialect.quoteIdentifier(snakeName),
307
- type: ddlType,
308
- maxLength: null,
419
+ type: typeInput.type,
420
+ maxLength: typeInput.maxLength,
309
421
  primaryKey: config.isPrimaryKey,
310
422
  unique: config.isUnique,
311
423
  notNull,
@@ -326,6 +438,10 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1
326
438
  if (config.check) {
327
439
  def += ` CHECK (${config.check})`;
328
440
  }
441
+ // The enum's labels, on an engine whose column type does not hold them.
442
+ if (enumColumn?.check) {
443
+ def += ` CHECK (${enumColumn.check})`;
444
+ }
329
445
  return def;
330
446
  }
331
447
  /**
@@ -352,15 +468,136 @@ function normalizeDefault(val) {
352
468
  if (/^-?\d+(\.\d+)?$/.test(val.trim())) {
353
469
  return val.trim();
354
470
  }
355
- // Simple single-quoted string literals (no semicolons, no SQL statement keywords)
356
- if (/^'[^']*'$/.test(val.trim())) {
357
- const inner = val.trim().slice(1, -1);
471
+ // Single-quoted string literals, a quote inside written doubled (`'it''s'`),
472
+ // optionally followed by a PostgreSQL cast as the catalog prints a default
473
+ // (`'x'::text`, `'{}'::jsonb`). The cast is dropped: the literal is assigned
474
+ // to the column, which converts it to the column's type on every engine,
475
+ // and the other engines have no `::`. No semicolons, no statement keywords.
476
+ const literal = defaultStringLiteral(val.trim());
477
+ if (literal !== null) {
478
+ const inner = literal.slice(1, -1);
358
479
  if (/[;]/.test(inner) || /\b(DROP|ALTER|CREATE|INSERT|UPDATE|DELETE|GRANT|REVOKE|TRUNCATE)\b/i.test(inner)) {
359
- throw new Error(`Suspicious default value: ${val}. String literals must not contain SQL statements.`);
480
+ throw new errors_js_1.ValidationError(`Suspicious default value: ${val}. String literals must not contain SQL statements.`);
360
481
  }
361
- return val.trim();
482
+ return literal;
483
+ }
484
+ throw new errors_js_1.ValidationError(`Unsupported default value: ${val}. Use a SQL function, numeric, string literal (a quote inside it written ` +
485
+ `twice, as in 'it''s'), or NULL.`);
486
+ }
487
+ /**
488
+ * The string literal a default is, with any trailing PostgreSQL cast removed,
489
+ * or null when it is not one. Both patterns are linear: `[^']` and `''` cannot
490
+ * match the same text, and a cast's words are separated by single spaces.
491
+ */
492
+ function defaultStringLiteral(val) {
493
+ const cast = /^('(?:[^']|'')*')::[a-z_][a-z0-9_]*(?: [a-z_][a-z0-9_]*)*(?:\(\d+(?:, ?\d+)?\))?(?:\[\])?$/i.exec(val);
494
+ if (cast)
495
+ return cast[1];
496
+ return /^'(?:[^']|'')*'$/.test(val) ? val : null;
497
+ }
498
+ /**
499
+ * MySQL column types that accept no literal default: a default has to be written
500
+ * as an expression, in parentheses (8.0.13+), even when its value is a constant.
501
+ */
502
+ const MYSQL_EXPRESSION_DEFAULT_TYPES = /^(TINY|MEDIUM|LONG)?(TEXT|BLOB)\b|^JSON\b/i;
503
+ /**
504
+ * A MySQL expression default for a string literal, as its UTF-8 bytes in hex:
505
+ * `(X'...')` on a BLOB column, `(CONVERT(X'...' USING utf8mb4))` on a TEXT or
506
+ * JSON one. `normalized` is a single-quoted literal with its quotes doubled.
507
+ */
508
+ function mysqlHexDefault(normalized, columnType) {
509
+ const value = normalized.slice(1, -1).replace(/''/g, "'");
510
+ let hex = '';
511
+ for (const byte of new TextEncoder().encode(value))
512
+ hex += byte.toString(16).padStart(2, '0');
513
+ return /BLOB/i.test(columnType) ? `(X'${hex}')` : `(CONVERT(X'${hex}' USING utf8mb4))`;
514
+ }
515
+ /** What a clock default means on a MySQL date or time column. */
516
+ function mysqlClockDefault(key, ddlType) {
517
+ const isClock = key === 'NOW()' || key === 'CURRENT_TIMESTAMP' || key === 'CURRENT_DATE' || key === 'CURRENT_TIME';
518
+ if (!isClock)
519
+ return null;
520
+ const datetime = /^(?:DATETIME|TIMESTAMP)(?:\((\d)\))?$/i.exec(ddlType);
521
+ if (datetime) {
522
+ // MySQL 8 rejects a default whose fractional precision differs from the
523
+ // column's, and a bare CURRENT_DATE / CURRENT_TIME is not a valid default
524
+ // for a DATETIME (only the parenthesized expression is).
525
+ if (key === 'NOW()' || key === 'CURRENT_TIMESTAMP') {
526
+ return datetime[1] ? `CURRENT_TIMESTAMP(${datetime[1]})` : 'CURRENT_TIMESTAMP';
527
+ }
528
+ return `(${key})`;
529
+ }
530
+ // DATE and TIME columns accept neither NOW() nor a bare CURRENT_*, so the
531
+ // default is the matching clock function as an expression.
532
+ if (/^DATE$/i.test(ddlType))
533
+ return '(CURRENT_DATE)';
534
+ if (/^TIME(?:\(\d\))?$/i.test(ddlType))
535
+ return '(CURRENT_TIME)';
536
+ return null;
537
+ }
538
+ /**
539
+ * The spelling of an already-normalized default for `dialect`. The schema
540
+ * vocabulary (`now()`, `true`, `gen_random_uuid()`) is Postgres's, and some of
541
+ * it is not SQL on the other engines: SQLite has no `NOW()` default, SQLite and
542
+ * MySQL have no `GEN_RANDOM_UUID()`, and SQL Server has none of `NOW()`, boolean
543
+ * literals or `CURRENT_DATE`. A literal (number, string, NULL) is the same
544
+ * everywhere and passes through, except on a MySQL TEXT, BLOB or JSON column,
545
+ * where it has to be an expression. `columnType` is the column's MySQL type as
546
+ * the dialect spells it (`DATETIME(3)`, `TEXT`); other dialects ignore it.
547
+ */
548
+ function dialectDefault(normalized, dialect, columnType = '') {
549
+ const key = normalized.toUpperCase();
550
+ switch (dialect.name) {
551
+ case 'sqlite':
552
+ if (key === 'NOW()')
553
+ return 'CURRENT_TIMESTAMP';
554
+ if (key === 'GEN_RANDOM_UUID()') {
555
+ return "(lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)), 2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)), 2) || '-' || hex(randomblob(6))))";
556
+ }
557
+ return normalized;
558
+ case 'mysql': {
559
+ if (key === 'GEN_RANDOM_UUID()')
560
+ return '(UUID())';
561
+ const clock = mysqlClockDefault(key, columnType);
562
+ if (clock)
563
+ return clock;
564
+ // `DEFAULT NULL` is the one default these types accept as a literal.
565
+ const expression = key !== 'NULL' && MYSQL_EXPRESSION_DEFAULT_TYPES.test(columnType);
566
+ if (normalized.startsWith("'") && /['\\]/.test(normalized.slice(1, -1))) {
567
+ // A quote or a backslash inside the literal. Whether `\` escapes depends
568
+ // on the session's NO_BACKSLASH_ESCAPES, which Turbine's own connections
569
+ // set and other clients do not. An EXPRESSION default is worse: MySQL
570
+ // stores it re-escaped with backslashes and parses it again on every
571
+ // insert under the inserting session's mode, so `('it''s')` created
572
+ // fine and then failed every insert on a Turbine connection (measured
573
+ // on MySQL 8). Spelled as hex bytes, the value has no quote or
574
+ // backslash to read either way.
575
+ if (expression)
576
+ return mysqlHexDefault(normalized, columnType);
577
+ if (normalized.includes('\\')) {
578
+ throw new errors_js_1.ValidationError(`The default ${normalized} holds a backslash, which MySQL reads differently with and without ` +
579
+ 'NO_BACKSLASH_ESCAPES. Use a default without one.');
580
+ }
581
+ }
582
+ if (expression)
583
+ return `(${normalized})`;
584
+ return normalized;
585
+ }
586
+ case 'mssql':
587
+ if (key === 'NOW()')
588
+ return 'CURRENT_TIMESTAMP';
589
+ if (key === 'GEN_RANDOM_UUID()')
590
+ return 'NEWID()';
591
+ if (key === 'TRUE')
592
+ return '1';
593
+ if (key === 'FALSE')
594
+ return '0';
595
+ if (key === 'CURRENT_DATE')
596
+ return '(CAST(SYSDATETIME() AS DATE))';
597
+ return normalized;
598
+ default:
599
+ return normalized;
362
600
  }
363
- throw new Error(`Unsupported default value: ${val}. Use a SQL function, numeric, string literal, or NULL.`);
364
601
  }
365
602
  /**
366
603
  * Generate CREATE INDEX statements for foreign key columns.
@@ -515,6 +752,16 @@ function generateDeclaredIndexes(table, dialect = dialect_js_1.postgresDialect)
515
752
  }
516
753
  return out;
517
754
  }
755
+ /**
756
+ * The warnings in a diff that report a real difference between the database and
757
+ * the schema file: everything in `warnings` except the entries the diff marked
758
+ * as possibly cosmetic ({@link DiffResult.inexactWarnings}). `push --dry-run
759
+ * --exit-code` fails on these.
760
+ */
761
+ function driftWarnings(diff) {
762
+ const inexact = new Set(diff.inexactWarnings ?? []);
763
+ return (diff.warnings ?? []).filter((w) => !inexact.has(w));
764
+ }
518
765
  /**
519
766
  * Build the `ADD CONSTRAINT ... FOREIGN KEY` statement for a FK with the given
520
767
  * referential actions. Default (`no action`) clauses are omitted, matching how
@@ -545,18 +792,49 @@ function diffReferentialAction(table, db, schemaOnDelete, schemaOnUpdate, dialec
545
792
  return { statements: [drop, add], reverseStatements: [drop, reverseAdd] };
546
793
  }
547
794
  /**
548
- * Compute append-only enum value changes. Returns `ALTER TYPE ... ADD VALUE`
549
- * statements for labels present in the schema but not the DB (in order), plus a
550
- * destructive warning for any DB label the schema dropped or any reorder -
551
- * Postgres cannot remove or reorder enum values without recreating the type.
795
+ * Compute the enum changes needed to make the database's labels match the
796
+ * schema's, KEEPING THE DECLARED ORDER.
797
+ *
798
+ * An enum's order is observable: it decides `ORDER BY`, `<` and `MIN`/`MAX` on
799
+ * the column. A new label therefore goes where the schema declares it, not
800
+ * wherever the end of the type happens to be: `ADD VALUE 'x' AFTER 'prev'`, or
801
+ * `BEFORE 'next'` when it leads. A label that belongs at the end emits the bare
802
+ * `ADD VALUE`, which is what every earlier version emitted for it.
803
+ *
804
+ * Postgres cannot remove or reorder values in place, so a database label the
805
+ * schema dropped, and a declared order the database does not already have, are
806
+ * warnings (never statements): the type has to be recreated by hand.
552
807
  */
553
808
  function diffEnumValues(enumName, schemaLabels, dbLabels, dialect = dialect_js_1.postgresDialect) {
554
809
  const statements = [];
555
810
  const warnings = [];
556
- const dbSet = new Set(dbLabels);
557
- for (const label of schemaLabels) {
558
- if (!dbSet.has(label)) {
559
- statements.push(`ALTER TYPE ${dialect.quoteIdentifier(enumName)} ADD VALUE ${quoteEnumLabel(label)};`);
811
+ const type = dialect.quoteIdentifier(enumName);
812
+ // The type's label order as the statements below will leave it.
813
+ const order = [...dbLabels];
814
+ const schemaIndex = schemaLabels.map((label, i) => ({ label, i }));
815
+ for (const { label, i } of schemaIndex) {
816
+ if (order.includes(label))
817
+ continue;
818
+ const previous = i > 0 ? schemaLabels[i - 1] : undefined;
819
+ const next = schemaLabels.slice(i + 1).find((l) => order.includes(l));
820
+ const prevAt = previous === undefined ? -1 : order.indexOf(previous);
821
+ if (previous !== undefined && prevAt !== -1) {
822
+ if (prevAt === order.length - 1) {
823
+ statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)};`);
824
+ order.push(label);
825
+ }
826
+ else {
827
+ statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)} AFTER ${quoteEnumLabel(previous)};`);
828
+ order.splice(prevAt + 1, 0, label);
829
+ }
830
+ }
831
+ else if (next !== undefined) {
832
+ statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)} BEFORE ${quoteEnumLabel(next)};`);
833
+ order.splice(order.indexOf(next), 0, label);
834
+ }
835
+ else {
836
+ statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)};`);
837
+ order.push(label);
560
838
  }
561
839
  }
562
840
  const schemaSet = new Set(schemaLabels);
@@ -565,6 +843,14 @@ function diffEnumValues(enumName, schemaLabels, dbLabels, dialect = dialect_js_1
565
843
  warnings.push(`Enum "${enumName}": labels [${removed.join(', ')}] exist in the database but not the schema. ` +
566
844
  `Postgres cannot remove enum values in place, recreate the type manually if intended.`);
567
845
  }
846
+ // After the adds, the labels both sides share must already be in declared
847
+ // order; if not, no ADD VALUE can fix it.
848
+ const shared = order.filter((l) => schemaSet.has(l));
849
+ if (shared.some((label, i) => label !== schemaLabels[i])) {
850
+ warnings.push(`Enum "${enumName}": the schema declares the order [${schemaLabels.join(', ')}] but the database has ` +
851
+ `[${shared.join(', ')}]. Postgres cannot reorder enum values in place, recreate the type manually if the ` +
852
+ `order matters (it decides ORDER BY and comparisons on the column).`);
853
+ }
568
854
  return { statements, warnings };
569
855
  }
570
856
  /**
@@ -756,7 +1042,17 @@ async function schemaDiff(schema, connectionString, options = {}) {
756
1042
  const schemaDdlNames = new Set();
757
1043
  for (const def of Object.values(schema.tables))
758
1044
  schemaDdlNames.add(def.name);
759
- const result = { create: [], alter: [], drop: [], statements: [], reverseStatements: [], warnings: [] };
1045
+ const result = {
1046
+ create: [],
1047
+ alter: [],
1048
+ drop: [],
1049
+ statements: [],
1050
+ reverseStatements: [],
1051
+ warnings: [],
1052
+ inexactWarnings: [],
1053
+ widenings: [],
1054
+ notCompared: [],
1055
+ };
760
1056
  const resolveRef = makeRefResolver(schema);
761
1057
  // --- Enums: CREATE TYPE for new enums (before tables), ADD VALUE for grown
762
1058
  // ones, and a warning for any destructive removal/reorder. New CREATE
@@ -790,9 +1086,10 @@ async function schemaDiff(schema, connectionString, options = {}) {
790
1086
  result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${dialect.quoteIdentifier(ddlName)} CASCADE;`);
791
1087
  }
792
1088
  }
793
- // Tables to drop (in DB but not in schema)
1089
+ // Tables to drop (in DB but not in schema). Turbine's own bookkeeping tables
1090
+ // (`_turbine_migrations`, ...) are never the user's to drop.
794
1091
  for (const existingTable of existingTables) {
795
- if (!schemaDdlNames.has(existingTable)) {
1092
+ if (!schemaDdlNames.has(existingTable) && !existingTable.startsWith('_turbine_')) {
796
1093
  result.drop.push(existingTable);
797
1094
  // We don't auto-generate DROP statements for safety
798
1095
  }
@@ -806,6 +1103,7 @@ async function schemaDiff(schema, connectionString, options = {}) {
806
1103
  const dbCols = dbColumns[tableName] ?? {};
807
1104
  const tableUniques = dbUniques[tableName] ?? {};
808
1105
  const alterDef = { table: tableName, columns: [] };
1106
+ const sequenceDefaults = [];
809
1107
  for (const [fieldName, config] of Object.entries(tableDef.columns)) {
810
1108
  const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
811
1109
  const dbCol = dbCols[snakeName];
@@ -817,6 +1115,13 @@ async function schemaDiff(schema, connectionString, options = {}) {
817
1115
  alterDef.columns.push({ column: snakeName, action: 'add', sql, reverseSql });
818
1116
  result.statements.push(sql);
819
1117
  result.reverseStatements.unshift(reverseSql);
1118
+ // NOT NULL with nothing to fill existing rows with: Postgres runs the
1119
+ // statement fine on an empty table and rejects it on any other.
1120
+ if ((config.isNotNull || config.isPrimaryKey) && !isSerialType(config.type) && config.defaultValue == null) {
1121
+ result.warnings.push(`column "${tableName}"."${snakeName}" is added NOT NULL with no default: the ALTER fails on a table ` +
1122
+ `that already has rows. Give it a default, add it nullable first and backfill it, or scaffold the ` +
1123
+ `two-phase pattern with \`npx turbine migrate create <name> --recipe backfill\`.`);
1124
+ }
820
1125
  continue;
821
1126
  }
822
1127
  // Check type mismatch. Serial/bigserial columns are exempt: their
@@ -860,6 +1165,9 @@ async function schemaDiff(schema, connectionString, options = {}) {
860
1165
  alterDef.columns.push({ column: snakeName, action: 'alter_type', sql, reverseSql });
861
1166
  result.statements.push(sql);
862
1167
  result.reverseStatements.unshift(reverseSql);
1168
+ if (!typePlan.needsUsing && !typePlan.loss && isLosslessWidening(oldSqlType, sqlType)) {
1169
+ result.widenings.push({ statement: sql, from: oldSqlType, to: sqlType });
1170
+ }
863
1171
  // The destructive gate sees the statement and refuses by default; its
864
1172
  // label is necessarily generic ("cast may truncate or fail"). This
865
1173
  // warning names what THIS conversion does to THESE rows.
@@ -889,6 +1197,8 @@ async function schemaDiff(schema, connectionString, options = {}) {
889
1197
  if (!isSerial) {
890
1198
  const schemaDefault = config.defaultValue ? normalizeDefault(config.defaultValue) : null;
891
1199
  const dbDefault = dbCol.columnDefault;
1200
+ if (dbDefault && isSequenceDefault(dbDefault))
1201
+ sequenceDefaults.push(snakeName);
892
1202
  if (schemaDefault && !dbDefault) {
893
1203
  // Schema has default, DB doesn't
894
1204
  const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${schemaDefault};`;
@@ -946,18 +1256,23 @@ async function schemaDiff(schema, connectionString, options = {}) {
946
1256
  // gate, not by hiding the statement: withholding it here previously made
947
1257
  // `push` report "already in sync" for a destructive-only diff and print a
948
1258
  // false "Altered" for a mixed one, since the 0.36 guard never received it.
949
- // No reverse is emitted (a dropped column and its data cannot be recreated
950
- // automatically), so diff-generated migrations fall back to the documented
951
- // irreversible-DOWN placeholder for this change.
1259
+ // A dropped column and its data cannot be recreated automatically, so the
1260
+ // reverse is irreversibleDropPlaceholder: an IRREVERSIBLE note in the DOWN
1261
+ // section describing the column, never SQL that pretends to restore it.
952
1262
  for (const dbColName of Object.keys(dbCols)) {
953
1263
  const hasField = Object.entries(tableDef.columns).some(([fieldName]) => (0, schema_js_1.camelToSnake)(fieldName) === dbColName);
954
1264
  if (!hasField) {
955
1265
  const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(dbColName)};`;
956
- const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}"; add it back manually`;
1266
+ const reverseSql = irreversibleDropPlaceholder(tableName, dbColName, dbCols[dbColName], dialect);
957
1267
  alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
958
1268
  result.statements.push(sql);
1269
+ result.reverseStatements.unshift(reverseSql);
959
1270
  }
960
1271
  }
1272
+ if (sequenceDefaults.length > 0) {
1273
+ result.notCompared.push(`sequence-backed defaults on "${tableName}" (${sequenceDefaults.join(', ')}): the database default is a ` +
1274
+ `sequence, which is never compared or changed`);
1275
+ }
961
1276
  if (alterDef.columns.length > 0) {
962
1277
  result.alter.push(alterDef);
963
1278
  }
@@ -994,6 +1309,17 @@ async function schemaDiff(schema, connectionString, options = {}) {
994
1309
  .map((c) => ({ name: c.name, expression: c.expression }));
995
1310
  const tableDbChecks = dbChecks[tableName] ?? [];
996
1311
  const dbCheckByName = new Map(tableDbChecks.map((c) => [c.name, c]));
1312
+ const namedCheckNames = new Set(namedSchemaChecks.map((c) => c.name));
1313
+ const unmatchedChecks = tableDbChecks.filter((c) => !namedCheckNames.has(c.name)).map((c) => c.name);
1314
+ if (unmatchedChecks.length > 0) {
1315
+ result.notCompared.push(`CHECK constraints on "${tableName}" (${unmatchedChecks.join(', ')}): the schema does not name them, so ` +
1316
+ `they are not compared and never dropped`);
1317
+ }
1318
+ const unnamedSchemaChecks = (tableDef.checks ?? []).filter((c) => !c.name).length;
1319
+ if (unnamedSchemaChecks > 0) {
1320
+ result.notCompared.push(`${unnamedSchemaChecks} unnamed CHECK constraint(s) declared on "${tableName}": with no name there is nothing ` +
1321
+ `to match them against the database`);
1322
+ }
997
1323
  const normExpr = (e) => e.replace(/\s+/g, ' ').trim();
998
1324
  for (const sc of namedSchemaChecks) {
999
1325
  const existing = dbCheckByName.get(sc.name);
@@ -1006,9 +1332,11 @@ async function schemaDiff(schema, connectionString, options = {}) {
1006
1332
  result.reverseStatements.unshift(dropSql);
1007
1333
  }
1008
1334
  else if (normExpr(existing.expression) !== normExpr(sc.expression)) {
1009
- result.warnings.push(`check constraint "${sc.name}" on "${tableName}": stored expression differs from the schema text ` +
1335
+ const checkWarning = `check constraint "${sc.name}" on "${tableName}": stored expression differs from the schema text ` +
1010
1336
  `(Postgres canonicalizes CHECK expressions, so this is usually cosmetic). ` +
1011
- `To intentionally replace it, rename the constraint or drop/re-add it in a manual migration.`);
1337
+ `To intentionally replace it, rename the constraint or drop/re-add it in a manual migration.`;
1338
+ result.warnings.push(checkWarning);
1339
+ result.inexactWarnings.push(checkWarning);
1012
1340
  }
1013
1341
  }
1014
1342
  // --- User-declared indexes (TableDef.indexes) ---
@@ -1025,6 +1353,18 @@ async function schemaDiff(schema, connectionString, options = {}) {
1025
1353
  // worth surfacing. Deleting the last declared index must NOT silence the
1026
1354
  // warning pass. Tables with no `indexes` key stay silent (not managed).
1027
1355
  const declaredPlain = (tableDef.indexes ?? []).filter((i) => !(0, schema_builder_js_1.isDocFieldIndexDef)(i));
1356
+ if (tableDef.indexes === undefined) {
1357
+ const dbIdxNames = [...(dbIndexes[tableName]?.keys() ?? [])];
1358
+ const recognized = new Set(Object.values(dbUniques[tableName] ?? {}));
1359
+ for (const [fieldName, config] of Object.entries(tableDef.columns)) {
1360
+ if (config.referencesTarget)
1361
+ recognized.add(`idx_${tableName}_${(0, schema_js_1.camelToSnake)(fieldName)}`);
1362
+ }
1363
+ const unmanaged = dbIdxNames.filter((n) => !recognized.has(n) && !n.endsWith('_pkey'));
1364
+ if (unmanaged.length > 0) {
1365
+ result.notCompared.push(`indexes on "${tableName}" (${unmanaged.join(', ')}): the table declares no \`indexes\`, so they are not compared`);
1366
+ }
1367
+ }
1028
1368
  if (tableDef.indexes !== undefined) {
1029
1369
  const dbIdx = dbIndexes[tableName] ?? new Map();
1030
1370
  const dbIdxNames = new Set(dbIdx.keys());
@@ -1073,12 +1413,34 @@ async function schemaDiff(schema, connectionString, options = {}) {
1073
1413
  }
1074
1414
  }
1075
1415
  }
1416
+ result.notCompared.push(exports.NEVER_COMPARED);
1076
1417
  return result;
1077
1418
  }
1078
1419
  finally {
1079
1420
  await client.end();
1080
1421
  }
1081
1422
  }
1423
+ /**
1424
+ * What {@link schemaDiff} never looks at, whatever the database holds. Kept
1425
+ * beside the diff so the sentence cannot outlive the code it describes.
1426
+ */
1427
+ exports.NEVER_COMPARED = 'primary keys, multi-column UNIQUE constraints, and whether a foreign key exists on an existing column ' +
1428
+ '(only the referential actions of foreign keys that already exist are compared)';
1429
+ /**
1430
+ * The DOWN-side stand-in for a `DROP COLUMN`: a comment block, because the data
1431
+ * the column held cannot be put back by any statement. It names the column and
1432
+ * its old type so the person writing the real rollback has the facts, and it is
1433
+ * a comment rather than SQL so a rollback never invents a column of NULLs and
1434
+ * calls it restored.
1435
+ */
1436
+ function irreversibleDropPlaceholder(table, column, dbCol, dialect = dialect_js_1.postgresDialect) {
1437
+ const q = (name) => dialect.quoteIdentifier(name);
1438
+ return [
1439
+ `-- IRREVERSIBLE: this migration drops ${q(table)}.${q(column)}, and the data in it cannot be restored automatically.`,
1440
+ '-- To roll back, restore the data from a backup and re-add the column by hand, for example:',
1441
+ `-- ALTER TABLE ${q(table)} ADD COLUMN ${q(column)} ${udtToSqlType(dbCol)};`,
1442
+ ].join('\n');
1443
+ }
1082
1444
  /**
1083
1445
  * Rough type families. Within a family Postgres has an ASSIGNMENT cast between
1084
1446
  * every pair the schema builder can produce, which is what lets the diff emit a
@@ -1367,6 +1729,89 @@ function planTypeChange(config, dbCol, column) {
1367
1729
  }
1368
1730
  return { kind: 'none' };
1369
1731
  }
1732
+ const WIDENING_BASES = {
1733
+ VARCHAR: 'varchar',
1734
+ 'CHARACTER VARYING': 'varchar',
1735
+ TEXT: 'text',
1736
+ SMALLINT: 'int2',
1737
+ INT2: 'int2',
1738
+ INTEGER: 'int4',
1739
+ INT: 'int4',
1740
+ INT4: 'int4',
1741
+ BIGINT: 'int8',
1742
+ INT8: 'int8',
1743
+ NUMERIC: 'numeric',
1744
+ DECIMAL: 'numeric',
1745
+ };
1746
+ /** Digits an integer type can hold, for the integer-to-numeric(p, s) rule. */
1747
+ const INTEGER_DIGITS = { int2: 5, int4: 10, int8: 19 };
1748
+ /** Parse a SQL type string, or null for any type outside the widening vocabulary. */
1749
+ function parseSqlType(raw) {
1750
+ const m = /^([A-Za-z][A-Za-z0-9 ]*?)\s*(?:\(\s*(\d+)\s*(?:,\s*(\d+)\s*)?\))?\s*(\[\])?$/.exec(raw.trim().replace(/\s+/g, ' '));
1751
+ if (!m)
1752
+ return null;
1753
+ const base = WIDENING_BASES[m[1].toUpperCase()];
1754
+ if (!base)
1755
+ return null;
1756
+ const a = m[2] === undefined ? null : Number(m[2]);
1757
+ const b = m[3] === undefined ? (a === null ? null : 0) : Number(m[3]);
1758
+ if ((base === 'text' || base.startsWith('int')) && a !== null)
1759
+ return null;
1760
+ if (base === 'varchar' && m[3] !== undefined)
1761
+ return null;
1762
+ return { base, a, b: base === 'numeric' ? b : null, array: m[4] !== undefined };
1763
+ }
1764
+ /**
1765
+ * True when changing a column from `from` to `to` can never lose, round,
1766
+ * truncate or reject a value: every value of the old type is representable
1767
+ * unchanged in the new one. The vocabulary is deliberately small, and anything
1768
+ * outside it answers false (the destructive gate then treats the statement as
1769
+ * it always has):
1770
+ *
1771
+ * - `varchar(n)` to `varchar(m)` with m >= n, to bare `varchar`, to `text`
1772
+ * - `smallint` to `integer` to `bigint`
1773
+ * - any integer type to `numeric`, or to `numeric(p, s)` when it still holds
1774
+ * the integer's digits
1775
+ * - `numeric(p, s)` to a numeric with at least as many integer digits and at
1776
+ * least as many decimal places (a bare `numeric` has no bound)
1777
+ * - arrays of the above, element by element
1778
+ *
1779
+ * Not a widening, on purpose: `text` to `varchar(n)`, anything to a smaller
1780
+ * type, `timestamp` to `timestamptz` (re-reads the value in the session zone),
1781
+ * `real` to `double precision` (the stored digits change when printed), and
1782
+ * `json` to `jsonb` (drops duplicate keys).
1783
+ */
1784
+ function isLosslessWidening(from, to) {
1785
+ const f = parseSqlType(from);
1786
+ const t = parseSqlType(to);
1787
+ if (!f || !t || f.array !== t.array)
1788
+ return false;
1789
+ if (f.base === 'varchar') {
1790
+ if (t.base === 'text')
1791
+ return true;
1792
+ if (t.base !== 'varchar')
1793
+ return false;
1794
+ return t.a === null || (f.a !== null && t.a >= f.a);
1795
+ }
1796
+ if (f.base === 'text')
1797
+ return t.base === 'text';
1798
+ if (f.base === 'numeric') {
1799
+ if (t.base !== 'numeric')
1800
+ return false;
1801
+ if (t.a === null)
1802
+ return true;
1803
+ if (f.a === null)
1804
+ return false;
1805
+ return t.b >= f.b && t.a - t.b >= f.a - f.b;
1806
+ }
1807
+ // Integer source.
1808
+ const digits = INTEGER_DIGITS[f.base];
1809
+ if (t.base === 'numeric')
1810
+ return t.a === null || t.a - t.b >= digits;
1811
+ if (!t.base.startsWith('int'))
1812
+ return false;
1813
+ return INTEGER_DIGITS[t.base] >= digits;
1814
+ }
1370
1815
  /**
1371
1816
  * Map a schema column type to its expected PostgreSQL UDT name.
1372
1817
  */
@@ -1490,10 +1935,17 @@ function defaultsMatch(schemaDefault, dbDefault) {
1490
1935
  * `ALTER COLUMN ... TYPE` cast, exactly the kind of silent data loss `push`
1491
1936
  * must never apply without an explicit opt-in.
1492
1937
  */
1493
- function findDestructivePushStatements(statements) {
1938
+ function findDestructivePushStatements(statements, widenings = []) {
1939
+ // A lossless widening is the one `ALTER COLUMN ... TYPE` that destroys nothing.
1940
+ // It is skipped by statement identity, so a hand-edited or look-alike
1941
+ // statement is still scanned.
1942
+ const lossless = new Set(widenings.map((w) => w.statement));
1494
1943
  const hits = [];
1495
- for (const stmt of statements)
1944
+ for (const stmt of statements) {
1945
+ if (lossless.has(stmt))
1946
+ continue;
1496
1947
  hits.push(...(0, destructive_js_1.scanDestructiveSql)(stmt));
1948
+ }
1497
1949
  return hits;
1498
1950
  }
1499
1951
  /**
@@ -1633,7 +2085,7 @@ async function schemaPush(schema, connectionString, options = {}) {
1633
2085
  }
1634
2086
  // Destructive-statement gate: refuse silent data loss unless opted in.
1635
2087
  if (!options.allowDestructive) {
1636
- const destructive = findDestructivePushStatements(diff.statements);
2088
+ const destructive = findDestructivePushStatements(diff.statements, diff.widenings);
1637
2089
  if (destructive.length > 0) {
1638
2090
  throw new DestructivePushRefusal(destructive);
1639
2091
  }