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
@@ -14,6 +14,7 @@
14
14
  import type { DatabaseAdapter } from '../adapters/index.js';
15
15
  import { type Dialect } from '../dialect.js';
16
16
  import type { PgCompatQueryResult } from '../pg-types.js';
17
+ import { type TypeWidening } from '../schema-sql.js';
17
18
  import { type DestructiveStatement } from './destructive.js';
18
19
  import { splitSqlStatements } from './sql-statements.js';
19
20
  /**
@@ -111,6 +112,14 @@ export declare function migrationTimestamp(name: string): string | null;
111
112
  * @internal exported for tests.
112
113
  */
113
114
  export declare function assertNoEmbeddedTransactions(files: MigrationFile[], section: 'up' | 'down'): void;
115
+ /** The marker line for a {@link TypeWidening}. */
116
+ export declare function wideningMarker(w: Pick<TypeWidening, 'from' | 'to'>): string;
117
+ /**
118
+ * {@link scanDestructiveSql}, minus the `ALTER COLUMN ... TYPE` statements that
119
+ * carry a checked widening marker. Everything else is reported exactly as the
120
+ * scanner reports it.
121
+ */
122
+ export declare function scanMigrationDestructive(sql: string): DestructiveStatement[];
114
123
  /** Scan a set of migration files' UP sections for data-destroying statements. */
115
124
  export declare function collectUpDestructive(files: MigrationFile[]): DestructiveOffender[];
116
125
  /**
@@ -166,7 +175,14 @@ export declare function headerSafeName(name: string): string;
166
175
  */
167
176
  export declare function sanitizeName(name: string): string;
168
177
  /**
169
- * Generate a YYYYMMDDHHMMSS timestamp string from a Date.
178
+ * Generate a YYYYMMDDHHMMSS timestamp string from a Date, in UTC.
179
+ *
180
+ * Apply order is the filename order, so two people who create migrations in
181
+ * different time zones must stamp the same instant with the same digits. In local
182
+ * time a migration made at 09:00 in Berlin (07:00 UTC) sorted AFTER one made at
183
+ * 08:00 in New York (13:00 UTC) the same morning, and ran out of creation order.
184
+ * Files already on disk keep their local-time names; the runner reads any 14
185
+ * digits.
170
186
  */
171
187
  export declare function formatTimestamp(date: Date): string;
172
188
  /**
@@ -181,10 +197,14 @@ export declare function listMigrationFiles(migrationsDir: string): MigrationFile
181
197
  /**
182
198
  * `.sql` files in `migrationsDir` that the runner NEVER sees, because their
183
199
  * name lacks the `YYYYMMDDHHMMSS_<name>.sql` shape {@link listMigrationFiles}
184
- * requires (a hand-written `add_thing.sql`, a 13-digit prefix). Such a file
185
- * used to be skipped in silence by `status`, `up` and `deploy` alike, so a
186
- * misnamed migration simply never ran and nothing said so. The CLI prints one
187
- * warning per entry.
200
+ * requires (a hand-written `add_thing.sql`, a 13-digit prefix, an upper-case
201
+ * `.SQL` extension). Such a file used to be skipped in silence by `status`,
202
+ * `up` and `deploy` alike, so a misnamed migration simply never ran and
203
+ * nothing said so. The CLI prints one warning per entry.
204
+ *
205
+ * The extension is matched in any case here and only in lower case by the
206
+ * runner: `20260101000000_init.SQL` is a migration somebody meant to run, so
207
+ * it is reported rather than ignored like a `.txt` file.
188
208
  */
189
209
  export declare function listIgnoredSqlFiles(migrationsDir: string): string[];
190
210
  /** The parsed sections of a migration file plus its execution directives. */
@@ -283,6 +303,11 @@ export interface DiffMigrationBody {
283
303
  * when any exist, a loud file-level banner is prepended to UP.
284
304
  * - Any diff `warnings` (changes the diff refuses to apply automatically, e.g.
285
305
  * enum value removals) are surfaced as `-- NOTE:` comments in UP.
306
+ * - A type change the diff proved lossless (`widenings`) is not flagged: it is
307
+ * preceded by a {@link wideningMarker}, which `migrate up` checks before it
308
+ * stops excusing the statement.
309
+ * - Every `DROP COLUMN` in UP has a comment block in DOWN saying the data
310
+ * cannot be restored automatically (the diff supplies it as a reverse entry).
286
311
  *
287
312
  * Pure and DB-free, so it is unit-testable from a synthesized diff.
288
313
  */
@@ -290,6 +315,7 @@ export declare function buildDiffMigrationBody(diff: {
290
315
  statements: string[];
291
316
  reverseStatements: string[];
292
317
  warnings?: string[];
318
+ widenings?: readonly TypeWidening[];
293
319
  }): DiffMigrationBody;
294
320
  /**
295
321
  * Create a new migration file.
@@ -479,6 +505,32 @@ export interface MigrationLock {
479
505
  * @internal exported for tests.
480
506
  */
481
507
  export declare function acquireMigrationLock(runner: MigrationLockClient, lockId: number, adapter: DatabaseAdapter | undefined, openLockConnection: () => Promise<MigrationLockClient>): Promise<MigrationLock>;
508
+ /** How long, and how, {@link acquireMigrationLockWaiting} waits for a held lock. */
509
+ export interface MigrationLockWait {
510
+ /** The longest total wait, in milliseconds. */
511
+ timeoutMs: number;
512
+ /** The pause between attempts, in milliseconds. Default 1000. */
513
+ intervalMs?: number;
514
+ /** Called once, on the first refusal, so the CLI can say it is waiting. */
515
+ onWait?: () => void;
516
+ /** @internal clock and sleep, replaced by tests. */
517
+ now?: () => number;
518
+ /** @internal */
519
+ sleep?: (ms: number) => Promise<void>;
520
+ }
521
+ /**
522
+ * {@link acquireMigrationLock}, retried until `wait.timeoutMs` has passed.
523
+ *
524
+ * Polls the adapter's own non-blocking acquire rather than issuing a blocking
525
+ * `pg_advisory_lock`: a blocking wait needs a `lock_timeout` to be bounded, and
526
+ * setting one is a `SET`, which must never run on a connection that may sit
527
+ * behind a pooler. Polling needs nothing but the statement the adapter already
528
+ * uses, and works for the lock-table adapters too. Without `wait` it is exactly
529
+ * one attempt, which is what every caller did before.
530
+ *
531
+ * @internal exported for tests.
532
+ */
533
+ export declare function acquireMigrationLockWaiting(runner: MigrationLockClient, lockId: number, adapter: DatabaseAdapter | undefined, openLockConnection: () => Promise<MigrationLockClient>, wait?: MigrationLockWait): Promise<MigrationLock>;
482
534
  /**
483
535
  * Release a lock taken by {@link acquireMigrationLock}, and close the dedicated
484
536
  * connection when there is one. A refused lock owns nothing, so releasing it is
@@ -598,6 +650,18 @@ export declare function formatChecksumMismatchError(mismatches: ChecksumMismatch
598
650
  * @internal exported for tests.
599
651
  */
600
652
  export declare function assertUpHasStatements(files: MigrationFile[]): void;
653
+ /** True when `sql` holds at least one statement that is not just comments. */
654
+ export declare function hasExecutableStatement(sql: string): boolean;
655
+ /**
656
+ * The files among `files` whose DOWN section runs no SQL at all (comments only).
657
+ *
658
+ * A diff-generated migration that drops a column ships a commented IRREVERSIBLE
659
+ * note as its DOWN, because no statement can bring the data back. Rolling such a
660
+ * migration back succeeds, removes its tracking row, and restores nothing, so
661
+ * the CLI names these files after a rollback instead of letting "rolled back"
662
+ * stand unqualified.
663
+ */
664
+ export declare function rollbacksWithoutSql(files: readonly MigrationFile[]): MigrationFile[];
601
665
  /**
602
666
  * Build a deploy plan from local migration files and applied migration rows.
603
667
  * This is pure file-system planning; callers with a database connection should
@@ -642,6 +706,14 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
642
706
  * SQL and the tracking table alike. See {@link connectionStringForSchema}.
643
707
  */
644
708
  schema?: string;
709
+ /**
710
+ * Wait up to this many milliseconds for another migration to release the
711
+ * lock, instead of failing at once. The pending list is read AFTER the lock
712
+ * is taken, so whatever the other run applied is simply no longer pending.
713
+ */
714
+ lockWaitMs?: number;
715
+ /** Called once when the lock is held by someone else and the wait begins. */
716
+ onLockWait?: () => void;
645
717
  }): Promise<MigrationRunResult>;
646
718
  /**
647
719
  * Production migration apply. This intentionally applies files as written and
@@ -651,6 +723,9 @@ export declare function migrateDeploy(connectionString: string, migrationsDir: s
651
723
  adapter?: DatabaseAdapter;
652
724
  allowDrift?: boolean;
653
725
  schema?: string;
726
+ /** Wait up to this many milliseconds for a concurrent deploy to finish. See `migrateUp`. */
727
+ lockWaitMs?: number;
728
+ onLockWait?: () => void;
654
729
  }): Promise<MigrationRunResult>;
655
730
  /**
656
731
  * Roll back a prepared LIFO batch, newest first, stopping at the first
@@ -738,3 +813,36 @@ export declare function migrateDown(connectionString: string, migrationsDir: str
738
813
  export declare function migrateStatus(connectionString: string, migrationsDir: string, options?: {
739
814
  schema?: string;
740
815
  }): Promise<MigrationStatus[]>;
816
+ /** The migration files in `statuses` that have not been applied to the database. */
817
+ export declare function pendingMigrationFiles(statuses: readonly MigrationStatus[]): MigrationFile[];
818
+ /**
819
+ * The refusal `migrate create --auto` / `--from-diff` prints while migrations
820
+ * are still pending.
821
+ *
822
+ * Both flags diff the schema file against the LIVE database. A pending migration
823
+ * is a change the file already describes and the database does not have yet, so
824
+ * the diff reports it AGAIN: the new migration repeats those statements and
825
+ * fails on the first database that applies the pending one before it.
826
+ */
827
+ export declare function formatPendingAutoRefusal(pending: readonly MigrationFile[], flag: string): string;
828
+ /** What `migrate status --exit-code` decides from. */
829
+ export interface MigrationHealth {
830
+ /** Migration files not yet applied. */
831
+ pending: number;
832
+ /** Applied migrations whose file has changed since. */
833
+ drifted: number;
834
+ /** Applied migrations whose file is gone. */
835
+ missing: number;
836
+ /** True only when all three are zero. */
837
+ ok: boolean;
838
+ }
839
+ /**
840
+ * Every reason `migrate status --exit-code` exits 2, as phrases for the message.
841
+ * Empty means the gate passes. `ignoredFiles` are the `.sql` files in the
842
+ * migrations directory whose name the runner never matches (see
843
+ * {@link listIgnoredSqlFiles}): a migration that will never run is as much a
844
+ * disagreement between the files and the database as a pending one.
845
+ */
846
+ export declare function statusGateProblems(health: MigrationHealth, ignoredFiles: readonly string[]): string[];
847
+ /** Count the ways a migration history can disagree with the files on disk. */
848
+ export declare function summarizeMigrationStatus(statuses: readonly MigrationStatus[]): MigrationHealth;
@@ -19,6 +19,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.MIGRATION_RECIPES = exports.splitSqlStatements = void 0;
20
20
  exports.migrationTimestamp = migrationTimestamp;
21
21
  exports.assertNoEmbeddedTransactions = assertNoEmbeddedTransactions;
22
+ exports.wideningMarker = wideningMarker;
23
+ exports.scanMigrationDestructive = scanMigrationDestructive;
22
24
  exports.collectUpDestructive = collectUpDestructive;
23
25
  exports.ensureTrackingTable = ensureTrackingTable;
24
26
  exports.readAppliedMigrations = readAppliedMigrations;
@@ -42,6 +44,7 @@ exports.assertPinnableSchema = assertPinnableSchema;
42
44
  exports.connectionStringForSchema = connectionStringForSchema;
43
45
  exports.assertSchemaExists = assertSchemaExists;
44
46
  exports.acquireMigrationLock = acquireMigrationLock;
47
+ exports.acquireMigrationLockWaiting = acquireMigrationLockWaiting;
45
48
  exports.releaseMigrationLock = releaseMigrationLock;
46
49
  exports.runMigrationInTransaction = runMigrationInTransaction;
47
50
  exports.predictOutOfOrder = predictOutOfOrder;
@@ -50,6 +53,8 @@ exports.compareAppliedChecksums = compareAppliedChecksums;
50
53
  exports.rebaselineChecksums = rebaselineChecksums;
51
54
  exports.formatChecksumMismatchError = formatChecksumMismatchError;
52
55
  exports.assertUpHasStatements = assertUpHasStatements;
56
+ exports.hasExecutableStatement = hasExecutableStatement;
57
+ exports.rollbacksWithoutSql = rollbacksWithoutSql;
53
58
  exports.planMigrationDeploy = planMigrationDeploy;
54
59
  exports.inspectMigrationDeploy = inspectMigrationDeploy;
55
60
  exports.migrateUp = migrateUp;
@@ -58,6 +63,10 @@ exports.rollbackMigrations = rollbackMigrations;
58
63
  exports.planMigrationRollback = planMigrationRollback;
59
64
  exports.migrateDown = migrateDown;
60
65
  exports.migrateStatus = migrateStatus;
66
+ exports.pendingMigrationFiles = pendingMigrationFiles;
67
+ exports.formatPendingAutoRefusal = formatPendingAutoRefusal;
68
+ exports.statusGateProblems = statusGateProblems;
69
+ exports.summarizeMigrationStatus = summarizeMigrationStatus;
61
70
  const node_crypto_1 = require("node:crypto");
62
71
  const node_fs_1 = require("node:fs");
63
72
  const node_path_1 = require("node:path");
@@ -67,6 +76,7 @@ const connection_guard_js_1 = require("../connection-guard.js");
67
76
  const connection_url_js_1 = require("../connection-url.js");
68
77
  const dialect_js_1 = require("../dialect.js");
69
78
  const errors_js_1 = require("../errors.js");
79
+ const schema_sql_js_1 = require("../schema-sql.js");
70
80
  const destructive_js_1 = require("./destructive.js");
71
81
  const sql_statements_js_1 = require("./sql-statements.js");
72
82
  Object.defineProperty(exports, "splitSqlStatements", { enumerable: true, get: function () { return sql_statements_js_1.splitSqlStatements; } });
@@ -117,6 +127,81 @@ function assertNoEmbeddedTransactions(files, section) {
117
127
  lines.push('add `-- turbine:no-transaction` to the file header and manage it yourself.');
118
128
  throw new errors_js_1.MigrationError(lines.join('\n'));
119
129
  }
130
+ // ---------------------------------------------------------------------------
131
+ // Lossless type widenings
132
+ // ---------------------------------------------------------------------------
133
+ /**
134
+ * The comment a diff-generated migration puts directly above an
135
+ * `ALTER COLUMN ... TYPE` that cannot lose a value: `-- turbine:widening
136
+ * <from> -> <to>`.
137
+ *
138
+ * `ALTER COLUMN ... TYPE` is a destructive statement to the scanner, which can
139
+ * read the new type and nothing else. The generator knows the old one, so it
140
+ * says what it knows in the file, and {@link scanMigrationDestructive} believes
141
+ * the claim only as far as it can check it: the pair must itself be a lossless
142
+ * widening ({@link isLosslessWidening}) and the statement below must change that
143
+ * one column to exactly the claimed type. It is an accident guard, like the rest
144
+ * of the scanner: a hand-written marker over a column whose real type differs
145
+ * from `from` is not caught.
146
+ */
147
+ const WIDENING_MARKER = /^--\s*turbine:widening\s+(.+?)\s*->\s*(.+?)\s*$/i;
148
+ /** `ALTER TABLE <t> ALTER [COLUMN] <c> [SET DATA] TYPE <type>` and nothing else. */
149
+ const SINGLE_ALTER_TYPE = /^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?(?:"[^"]+"|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|[A-Za-z_][\w$]*))?\s+ALTER\s+(?:COLUMN\s+)?(?:"[^"]+"|[A-Za-z_][\w$]*)\s+(?:SET\s+DATA\s+)?TYPE\s+((?:[^,();]|\([^)]*\))+?)\s*$/i;
150
+ /** The marker's claim from a statement's leading comment lines, or null. */
151
+ function leadingWideningClaim(raw) {
152
+ for (const line of raw.split('\n')) {
153
+ const t = line.trim();
154
+ if (t === '')
155
+ continue;
156
+ const m = WIDENING_MARKER.exec(t);
157
+ if (m)
158
+ return { from: m[1], to: m[2] };
159
+ if (!t.startsWith('--'))
160
+ break;
161
+ }
162
+ return null;
163
+ }
164
+ const normalizeTypeText = (t) => t.replace(/\s+/g, ' ').trim().toUpperCase();
165
+ /** The marker line for a {@link TypeWidening}. */
166
+ function wideningMarker(w) {
167
+ return `-- turbine:widening ${w.from} -> ${w.to}`;
168
+ }
169
+ /**
170
+ * {@link scanDestructiveSql}, minus the `ALTER COLUMN ... TYPE` statements that
171
+ * carry a checked widening marker. Everything else is reported exactly as the
172
+ * scanner reports it.
173
+ */
174
+ function scanMigrationDestructive(sql) {
175
+ const hits = (0, destructive_js_1.scanDestructiveSql)(sql);
176
+ if (!hits.some((h) => h.kind === 'alter-column-type'))
177
+ return hits;
178
+ // How many marked widenings of each statement text the file holds, so that one
179
+ // marked statement cannot excuse an identical unmarked one beside it.
180
+ const excused = new Map();
181
+ for (const st of (0, sql_statements_js_1.tokenizeSql)(sql)) {
182
+ if (st.commentOnly)
183
+ continue;
184
+ const claim = leadingWideningClaim(st.raw);
185
+ if (!claim || !(0, schema_sql_js_1.isLosslessWidening)(claim.from, claim.to))
186
+ continue;
187
+ const display = st.stripped.replace(/\s+/g, ' ');
188
+ const alter = SINGLE_ALTER_TYPE.exec(display.trim());
189
+ if (!alter || normalizeTypeText(alter[1]) !== normalizeTypeText(claim.to))
190
+ continue;
191
+ excused.set(display, (excused.get(display) ?? 0) + 1);
192
+ }
193
+ if (excused.size === 0)
194
+ return hits;
195
+ return hits.filter((h) => {
196
+ if (h.kind !== 'alter-column-type')
197
+ return true;
198
+ const left = excused.get(h.statement) ?? 0;
199
+ if (left === 0)
200
+ return true;
201
+ excused.set(h.statement, left - 1);
202
+ return false;
203
+ });
204
+ }
120
205
  /** Scan a set of migration files' UP sections for data-destroying statements. */
121
206
  function collectUpDestructive(files) {
122
207
  const offenders = [];
@@ -124,7 +209,7 @@ function collectUpDestructive(files) {
124
209
  const { up } = parseMigrationSQL(file.path);
125
210
  if (!up)
126
211
  continue;
127
- const hits = (0, destructive_js_1.scanDestructiveSql)(up);
212
+ const hits = scanMigrationDestructive(up);
128
213
  if (hits.length > 0)
129
214
  offenders.push({ file: file.filename, hits });
130
215
  }
@@ -281,16 +366,23 @@ function sanitizeName(name) {
281
366
  .replace(/^_|_$/g, '');
282
367
  }
283
368
  /**
284
- * Generate a YYYYMMDDHHMMSS timestamp string from a Date.
369
+ * Generate a YYYYMMDDHHMMSS timestamp string from a Date, in UTC.
370
+ *
371
+ * Apply order is the filename order, so two people who create migrations in
372
+ * different time zones must stamp the same instant with the same digits. In local
373
+ * time a migration made at 09:00 in Berlin (07:00 UTC) sorted AFTER one made at
374
+ * 08:00 in New York (13:00 UTC) the same morning, and ran out of creation order.
375
+ * Files already on disk keep their local-time names; the runner reads any 14
376
+ * digits.
285
377
  */
286
378
  function formatTimestamp(date) {
287
379
  return [
288
- date.getFullYear(),
289
- String(date.getMonth() + 1).padStart(2, '0'),
290
- String(date.getDate()).padStart(2, '0'),
291
- String(date.getHours()).padStart(2, '0'),
292
- String(date.getMinutes()).padStart(2, '0'),
293
- String(date.getSeconds()).padStart(2, '0'),
380
+ date.getUTCFullYear(),
381
+ String(date.getUTCMonth() + 1).padStart(2, '0'),
382
+ String(date.getUTCDate()).padStart(2, '0'),
383
+ String(date.getUTCHours()).padStart(2, '0'),
384
+ String(date.getUTCMinutes()).padStart(2, '0'),
385
+ String(date.getUTCSeconds()).padStart(2, '0'),
294
386
  ].join('');
295
387
  }
296
388
  /**
@@ -323,16 +415,20 @@ function listMigrationFiles(migrationsDir) {
323
415
  /**
324
416
  * `.sql` files in `migrationsDir` that the runner NEVER sees, because their
325
417
  * name lacks the `YYYYMMDDHHMMSS_<name>.sql` shape {@link listMigrationFiles}
326
- * requires (a hand-written `add_thing.sql`, a 13-digit prefix). Such a file
327
- * used to be skipped in silence by `status`, `up` and `deploy` alike, so a
328
- * misnamed migration simply never ran and nothing said so. The CLI prints one
329
- * warning per entry.
418
+ * requires (a hand-written `add_thing.sql`, a 13-digit prefix, an upper-case
419
+ * `.SQL` extension). Such a file used to be skipped in silence by `status`,
420
+ * `up` and `deploy` alike, so a misnamed migration simply never ran and
421
+ * nothing said so. The CLI prints one warning per entry.
422
+ *
423
+ * The extension is matched in any case here and only in lower case by the
424
+ * runner: `20260101000000_init.SQL` is a migration somebody meant to run, so
425
+ * it is reported rather than ignored like a `.txt` file.
330
426
  */
331
427
  function listIgnoredSqlFiles(migrationsDir) {
332
428
  if (!(0, node_fs_1.existsSync)(migrationsDir))
333
429
  return [];
334
430
  return (0, node_fs_1.readdirSync)(migrationsDir)
335
- .filter((f) => f.endsWith('.sql') && parseMigrationFilename(f) === null)
431
+ .filter((f) => /\.sql$/i.test(f) && parseMigrationFilename(f) === null)
336
432
  .sort();
337
433
  }
338
434
  /**
@@ -581,19 +677,50 @@ const DESTRUCTIVE_MIGRATION_BANNER = [
581
677
  '-- every flagged statement carefully before running.',
582
678
  '-- ============================================================',
583
679
  ];
680
+ /** Same banner when only the DOWN direction is destructive: UP runs without a prompt. */
681
+ const DESTRUCTIVE_DOWN_ONLY_BANNER = [
682
+ '-- ============================================================',
683
+ '-- WARNING: this migration contains DESTRUCTIVE statement(s).',
684
+ '-- Only the DOWN section is destructive (flagged inline there).',
685
+ '-- `turbine migrate up` runs UP as written; `turbine migrate down`',
686
+ '-- asks you to confirm before it runs the flagged statements.',
687
+ '-- ============================================================',
688
+ ];
689
+ /** True for a block that is nothing but comments and blank lines. */
690
+ function isCommentBlock(text) {
691
+ return text.split('\n').every((line) => {
692
+ const t = line.trim();
693
+ return t === '' || t.startsWith('--');
694
+ });
695
+ }
584
696
  /**
585
697
  * Annotate a list of SQL statements: scan each for data-destroying operations
586
698
  * (via {@link scanDestructiveSql}) and prefix any offender with loud, commented
587
699
  * warnings. Statements are left intact so the existing `migrate up` gate still
588
700
  * refuses them by default; the comments just make the danger visible on review.
701
+ *
702
+ * A comment-only entry (the DOWN placeholder for a dropped column) is written
703
+ * through untouched: it is not SQL, so it is neither scanned nor given a `;`.
704
+ * A statement the diff proved to be a lossless widening gets its
705
+ * {@link wideningMarker} instead of a warning, and is not scanned.
589
706
  */
590
- function annotateDiffStatements(statements) {
707
+ function annotateDiffStatements(statements, widenings = []) {
591
708
  const destructive = [];
592
709
  const out = [];
710
+ const widened = new Map(widenings.map((w) => [w.statement.trim(), w]));
593
711
  for (const raw of statements) {
594
712
  const stmt = raw.trim();
595
713
  if (!stmt)
596
714
  continue;
715
+ if (isCommentBlock(stmt)) {
716
+ out.push(stmt);
717
+ continue;
718
+ }
719
+ const widening = widened.get(stmt.endsWith(';') ? stmt : `${stmt};`);
720
+ if (widening) {
721
+ out.push(wideningMarker(widening), stmt.endsWith(';') ? stmt : `${stmt};`);
722
+ continue;
723
+ }
597
724
  const hits = (0, destructive_js_1.scanDestructiveSql)(stmt);
598
725
  if (hits.length > 0) {
599
726
  destructive.push(...hits);
@@ -616,18 +743,23 @@ function annotateDiffStatements(statements) {
616
743
  * when any exist, a loud file-level banner is prepended to UP.
617
744
  * - Any diff `warnings` (changes the diff refuses to apply automatically, e.g.
618
745
  * enum value removals) are surfaced as `-- NOTE:` comments in UP.
746
+ * - A type change the diff proved lossless (`widenings`) is not flagged: it is
747
+ * preceded by a {@link wideningMarker}, which `migrate up` checks before it
748
+ * stops excusing the statement.
749
+ * - Every `DROP COLUMN` in UP has a comment block in DOWN saying the data
750
+ * cannot be restored automatically (the diff supplies it as a reverse entry).
619
751
  *
620
752
  * Pure and DB-free, so it is unit-testable from a synthesized diff.
621
753
  */
622
754
  function buildDiffMigrationBody(diff) {
623
- const up = annotateDiffStatements(diff.statements);
755
+ const up = annotateDiffStatements(diff.statements, diff.widenings);
624
756
  const hasReverse = diff.reverseStatements.length > 0;
625
757
  const down = hasReverse
626
758
  ? annotateDiffStatements(diff.reverseStatements)
627
759
  : { text: '', destructive: [] };
628
760
  const upParts = [];
629
761
  if (up.destructive.length > 0 || down.destructive.length > 0) {
630
- upParts.push(...DESTRUCTIVE_MIGRATION_BANNER, '');
762
+ upParts.push(...(up.destructive.length > 0 ? DESTRUCTIVE_MIGRATION_BANNER : DESTRUCTIVE_DOWN_ONLY_BANNER), '');
631
763
  }
632
764
  if (diff.warnings && diff.warnings.length > 0) {
633
765
  for (const w of diff.warnings)
@@ -717,7 +849,18 @@ ${autoContent.down}
717
849
  -- Write your rollback SQL here
718
850
  `;
719
851
  }
720
- (0, node_fs_1.writeFileSync)(filePath, template, 'utf-8');
852
+ // `wx` (create, fail if the name exists): the name carries a one-second
853
+ // timestamp, so a second create with the same name in that second would
854
+ // otherwise silently replace the first file, edits included.
855
+ try {
856
+ (0, node_fs_1.writeFileSync)(filePath, template, { encoding: 'utf-8', flag: 'wx' });
857
+ }
858
+ catch (err) {
859
+ if (err.code !== 'EEXIST')
860
+ throw err;
861
+ throw new errors_js_1.MigrationError(`Migration file ${filename} already exists, and its name is timestamped to the second. ` +
862
+ 'Wait a second and run the command again, or choose a different name.');
863
+ }
721
864
  return {
722
865
  filename,
723
866
  path: filePath,
@@ -962,6 +1105,33 @@ async function acquireMigrationLock(runner, lockId, adapter, openLockConnection)
962
1105
  throw err;
963
1106
  }
964
1107
  }
1108
+ /**
1109
+ * {@link acquireMigrationLock}, retried until `wait.timeoutMs` has passed.
1110
+ *
1111
+ * Polls the adapter's own non-blocking acquire rather than issuing a blocking
1112
+ * `pg_advisory_lock`: a blocking wait needs a `lock_timeout` to be bounded, and
1113
+ * setting one is a `SET`, which must never run on a connection that may sit
1114
+ * behind a pooler. Polling needs nothing but the statement the adapter already
1115
+ * uses, and works for the lock-table adapters too. Without `wait` it is exactly
1116
+ * one attempt, which is what every caller did before.
1117
+ *
1118
+ * @internal exported for tests.
1119
+ */
1120
+ async function acquireMigrationLockWaiting(runner, lockId, adapter, openLockConnection, wait) {
1121
+ let lock = await acquireMigrationLock(runner, lockId, adapter, openLockConnection);
1122
+ if (lock.acquired || !wait)
1123
+ return lock;
1124
+ const now = wait.now ?? Date.now;
1125
+ const sleep = wait.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1126
+ const interval = wait.intervalMs ?? 1000;
1127
+ const deadline = now() + wait.timeoutMs;
1128
+ wait.onWait?.();
1129
+ while (!lock.acquired && now() < deadline) {
1130
+ await sleep(Math.max(0, Math.min(interval, deadline - now())));
1131
+ lock = await acquireMigrationLock(runner, lockId, adapter, openLockConnection);
1132
+ }
1133
+ return lock;
1134
+ }
965
1135
  /**
966
1136
  * Release a lock taken by {@link acquireMigrationLock}, and close the dedicated
967
1137
  * connection when there is one. A refused lock owns nothing, so releasing it is
@@ -1188,14 +1358,29 @@ action = 'apply pending migrations') {
1188
1358
  function assertUpHasStatements(files) {
1189
1359
  for (const file of files) {
1190
1360
  const { up } = parseMigrationSQL(file.path);
1191
- const executable = (0, sql_statements_js_1.tokenizeSql)(up).filter((s) => !s.commentOnly && s.stripped.trim() !== '');
1192
- if (executable.length > 0)
1361
+ if (hasExecutableStatement(up))
1193
1362
  continue;
1194
1363
  throw new errors_js_1.MigrationError(`Migration ${file.filename} has an UP section with no executable statement (comments only). ` +
1195
1364
  `Applying it would record the migration as applied while running nothing, and filling the file in ` +
1196
1365
  `afterwards would then be refused as drift. Write the UP section or delete the file.`);
1197
1366
  }
1198
1367
  }
1368
+ /** True when `sql` holds at least one statement that is not just comments. */
1369
+ function hasExecutableStatement(sql) {
1370
+ return (0, sql_statements_js_1.tokenizeSql)(sql).some((s) => !s.commentOnly && s.stripped.trim() !== '');
1371
+ }
1372
+ /**
1373
+ * The files among `files` whose DOWN section runs no SQL at all (comments only).
1374
+ *
1375
+ * A diff-generated migration that drops a column ships a commented IRREVERSIBLE
1376
+ * note as its DOWN, because no statement can bring the data back. Rolling such a
1377
+ * migration back succeeds, removes its tracking row, and restores nothing, so
1378
+ * the CLI names these files after a rollback instead of letting "rolled back"
1379
+ * stand unqualified.
1380
+ */
1381
+ function rollbacksWithoutSql(files) {
1382
+ return files.filter((f) => f.path !== '' && !hasExecutableStatement(parseMigrationSQL(f.path).down));
1383
+ }
1199
1384
  /**
1200
1385
  * Build a deploy plan from local migration files and applied migration rows.
1201
1386
  * This is pure file-system planning; callers with a database connection should
@@ -1281,9 +1466,13 @@ async function migrateUp(connectionString, migrationsDir, options) {
1281
1466
  // table-lock adapter gets its OWN connection so the per-migration
1282
1467
  // BEGIN/COMMIT below cannot end the transaction the lock lives in.
1283
1468
  const adapter = options?.adapter;
1284
- const lock = await acquireMigrationLock(client, lockId, adapter, () => openLockConnection(url));
1469
+ const lockWaitMs = options?.lockWaitMs;
1470
+ const lock = await acquireMigrationLockWaiting(client, lockId, adapter, () => openLockConnection(url), lockWaitMs !== undefined && lockWaitMs > 0 ? { timeoutMs: lockWaitMs, onWait: options?.onLockWait } : undefined);
1285
1471
  if (!lock.acquired) {
1286
- throw new errors_js_1.MigrationError('Could not acquire migration lock, another migration is already running');
1472
+ throw new errors_js_1.MigrationError(lockWaitMs !== undefined && lockWaitMs > 0
1473
+ ? `Could not acquire migration lock within ${Math.round(lockWaitMs / 1000)}s: another migration is still running. ` +
1474
+ 'Raise --wait if it is expected to take longer.'
1475
+ : 'Could not acquire migration lock, another migration is already running');
1287
1476
  }
1288
1477
  try {
1289
1478
  await ensureTrackingTable(client, dialect);
@@ -1425,6 +1614,8 @@ async function migrateDeploy(connectionString, migrationsDir, options) {
1425
1614
  allowDestructive: true,
1426
1615
  adapter: options?.adapter,
1427
1616
  schema: options?.schema,
1617
+ lockWaitMs: options?.lockWaitMs,
1618
+ onLockWait: options?.onLockWait,
1428
1619
  });
1429
1620
  }
1430
1621
  /**
@@ -1693,3 +1884,61 @@ async function migrateStatus(connectionString, migrationsDir, options) {
1693
1884
  await client.end();
1694
1885
  }
1695
1886
  }
1887
+ // ---------------------------------------------------------------------------
1888
+ // Status helpers (pending files, CI exit code)
1889
+ // ---------------------------------------------------------------------------
1890
+ /** The migration files in `statuses` that have not been applied to the database. */
1891
+ function pendingMigrationFiles(statuses) {
1892
+ return statuses.filter((st) => !st.applied).map((st) => st.file);
1893
+ }
1894
+ /**
1895
+ * The refusal `migrate create --auto` / `--from-diff` prints while migrations
1896
+ * are still pending.
1897
+ *
1898
+ * Both flags diff the schema file against the LIVE database. A pending migration
1899
+ * is a change the file already describes and the database does not have yet, so
1900
+ * the diff reports it AGAIN: the new migration repeats those statements and
1901
+ * fails on the first database that applies the pending one before it.
1902
+ */
1903
+ function formatPendingAutoRefusal(pending, flag) {
1904
+ const lines = [
1905
+ `Refusing to create a migration with ${flag}: ${pending.length} migration(s) in the migrations directory are not applied to this database yet:`,
1906
+ '',
1907
+ ...pending.map((f) => ` ${f.filename}`),
1908
+ '',
1909
+ `${flag} compares your schema file with the LIVE database. The pending file(s) above already describe part of`,
1910
+ 'that difference, so the new migration would repeat their statements and fail when they run first.',
1911
+ '',
1912
+ 'Apply them, then run this command again: npx turbine migrate up',
1913
+ `If they do not touch what the schema file describes (a data migration, say), pass --allow-pending.`,
1914
+ ];
1915
+ return lines.join('\n');
1916
+ }
1917
+ /**
1918
+ * Every reason `migrate status --exit-code` exits 2, as phrases for the message.
1919
+ * Empty means the gate passes. `ignoredFiles` are the `.sql` files in the
1920
+ * migrations directory whose name the runner never matches (see
1921
+ * {@link listIgnoredSqlFiles}): a migration that will never run is as much a
1922
+ * disagreement between the files and the database as a pending one.
1923
+ */
1924
+ function statusGateProblems(health, ignoredFiles) {
1925
+ const problems = [];
1926
+ if (health.pending > 0)
1927
+ problems.push(`${health.pending} pending`);
1928
+ if (health.drifted > 0)
1929
+ problems.push(`${health.drifted} modified after being applied`);
1930
+ if (health.missing > 0)
1931
+ problems.push(`${health.missing} missing from disk`);
1932
+ if (ignoredFiles.length > 0) {
1933
+ problems.push(`${ignoredFiles.length} misnamed .sql file(s) that will never run (${ignoredFiles.join(', ')}; ` +
1934
+ `a migration filename is YYYYMMDDHHMMSS_name.sql)`);
1935
+ }
1936
+ return problems;
1937
+ }
1938
+ /** Count the ways a migration history can disagree with the files on disk. */
1939
+ function summarizeMigrationStatus(statuses) {
1940
+ const pending = statuses.filter((st) => !st.applied).length;
1941
+ const missing = statuses.filter((st) => st.missingFile).length;
1942
+ const drifted = statuses.filter((st) => st.applied && st.checksumValid === false && !st.missingFile).length;
1943
+ return { pending, drifted, missing, ok: pending === 0 && drifted === 0 && missing === 0 };
1944
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { type IncomingMessage, type ServerResponse } from 'node:http';
9
9
  import type { PgCompatPool } from '../pg-types.js';
10
+ import { type TicketGate } from './local-server-guard.js';
10
11
  export interface ObserveOptions {
11
12
  url: string;
12
13
  port: number;
@@ -15,11 +16,15 @@ export interface ObserveOptions {
15
16
  }
16
17
  export interface ObserveServerHandle {
17
18
  dispose: () => Promise<void>;
19
+ /** The session credential (header or cookie). Never part of `url`. */
18
20
  authToken: string;
21
+ /** Printable URL carrying a ONE-TIME login ticket, exchanged for the session cookie on first load. */
19
22
  url: string;
20
23
  }
21
24
  export declare function startObserve(options: ObserveOptions): Promise<ObserveServerHandle>;
22
25
  export declare function handleRequest(req: IncomingMessage, res: ServerResponse, pool: PgCompatPool, options: ObserveOptions, authToken: string, rateLimiter: Map<string, {
23
26
  count: number;
24
27
  resetAt: number;
25
- }>): Promise<void>;
28
+ }>,
29
+ /** The one-time login ticket. Absent on a hand-built call: the ticket is then `authToken` itself. */
30
+ ticketGate?: TicketGate): Promise<void>;