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;
@@ -20,6 +20,7 @@ import { guardConnection } from '../connection-guard.js';
20
20
  import { isPlainSchemaIdentifier, parseSearchPathValue, withSearchPathOption } from '../connection-url.js';
21
21
  import { postgresDialect } from '../dialect.js';
22
22
  import { MigrationError, ValidationError } from '../errors.js';
23
+ import { isLosslessWidening } from '../schema-sql.js';
23
24
  import { DESTRUCTIVE_KIND_LABEL, scanDestructiveSql } from './destructive.js';
24
25
  import { splitSqlStatements, tokenizeSql } from './sql-statements.js';
25
26
  /**
@@ -76,6 +77,81 @@ export function assertNoEmbeddedTransactions(files, section) {
76
77
  lines.push('add `-- turbine:no-transaction` to the file header and manage it yourself.');
77
78
  throw new MigrationError(lines.join('\n'));
78
79
  }
80
+ // ---------------------------------------------------------------------------
81
+ // Lossless type widenings
82
+ // ---------------------------------------------------------------------------
83
+ /**
84
+ * The comment a diff-generated migration puts directly above an
85
+ * `ALTER COLUMN ... TYPE` that cannot lose a value: `-- turbine:widening
86
+ * <from> -> <to>`.
87
+ *
88
+ * `ALTER COLUMN ... TYPE` is a destructive statement to the scanner, which can
89
+ * read the new type and nothing else. The generator knows the old one, so it
90
+ * says what it knows in the file, and {@link scanMigrationDestructive} believes
91
+ * the claim only as far as it can check it: the pair must itself be a lossless
92
+ * widening ({@link isLosslessWidening}) and the statement below must change that
93
+ * one column to exactly the claimed type. It is an accident guard, like the rest
94
+ * of the scanner: a hand-written marker over a column whose real type differs
95
+ * from `from` is not caught.
96
+ */
97
+ const WIDENING_MARKER = /^--\s*turbine:widening\s+(.+?)\s*->\s*(.+?)\s*$/i;
98
+ /** `ALTER TABLE <t> ALTER [COLUMN] <c> [SET DATA] TYPE <type>` and nothing else. */
99
+ 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;
100
+ /** The marker's claim from a statement's leading comment lines, or null. */
101
+ function leadingWideningClaim(raw) {
102
+ for (const line of raw.split('\n')) {
103
+ const t = line.trim();
104
+ if (t === '')
105
+ continue;
106
+ const m = WIDENING_MARKER.exec(t);
107
+ if (m)
108
+ return { from: m[1], to: m[2] };
109
+ if (!t.startsWith('--'))
110
+ break;
111
+ }
112
+ return null;
113
+ }
114
+ const normalizeTypeText = (t) => t.replace(/\s+/g, ' ').trim().toUpperCase();
115
+ /** The marker line for a {@link TypeWidening}. */
116
+ export function wideningMarker(w) {
117
+ return `-- turbine:widening ${w.from} -> ${w.to}`;
118
+ }
119
+ /**
120
+ * {@link scanDestructiveSql}, minus the `ALTER COLUMN ... TYPE` statements that
121
+ * carry a checked widening marker. Everything else is reported exactly as the
122
+ * scanner reports it.
123
+ */
124
+ export function scanMigrationDestructive(sql) {
125
+ const hits = scanDestructiveSql(sql);
126
+ if (!hits.some((h) => h.kind === 'alter-column-type'))
127
+ return hits;
128
+ // How many marked widenings of each statement text the file holds, so that one
129
+ // marked statement cannot excuse an identical unmarked one beside it.
130
+ const excused = new Map();
131
+ for (const st of tokenizeSql(sql)) {
132
+ if (st.commentOnly)
133
+ continue;
134
+ const claim = leadingWideningClaim(st.raw);
135
+ if (!claim || !isLosslessWidening(claim.from, claim.to))
136
+ continue;
137
+ const display = st.stripped.replace(/\s+/g, ' ');
138
+ const alter = SINGLE_ALTER_TYPE.exec(display.trim());
139
+ if (!alter || normalizeTypeText(alter[1]) !== normalizeTypeText(claim.to))
140
+ continue;
141
+ excused.set(display, (excused.get(display) ?? 0) + 1);
142
+ }
143
+ if (excused.size === 0)
144
+ return hits;
145
+ return hits.filter((h) => {
146
+ if (h.kind !== 'alter-column-type')
147
+ return true;
148
+ const left = excused.get(h.statement) ?? 0;
149
+ if (left === 0)
150
+ return true;
151
+ excused.set(h.statement, left - 1);
152
+ return false;
153
+ });
154
+ }
79
155
  /** Scan a set of migration files' UP sections for data-destroying statements. */
80
156
  export function collectUpDestructive(files) {
81
157
  const offenders = [];
@@ -83,7 +159,7 @@ export function collectUpDestructive(files) {
83
159
  const { up } = parseMigrationSQL(file.path);
84
160
  if (!up)
85
161
  continue;
86
- const hits = scanDestructiveSql(up);
162
+ const hits = scanMigrationDestructive(up);
87
163
  if (hits.length > 0)
88
164
  offenders.push({ file: file.filename, hits });
89
165
  }
@@ -240,16 +316,23 @@ export function sanitizeName(name) {
240
316
  .replace(/^_|_$/g, '');
241
317
  }
242
318
  /**
243
- * Generate a YYYYMMDDHHMMSS timestamp string from a Date.
319
+ * Generate a YYYYMMDDHHMMSS timestamp string from a Date, in UTC.
320
+ *
321
+ * Apply order is the filename order, so two people who create migrations in
322
+ * different time zones must stamp the same instant with the same digits. In local
323
+ * time a migration made at 09:00 in Berlin (07:00 UTC) sorted AFTER one made at
324
+ * 08:00 in New York (13:00 UTC) the same morning, and ran out of creation order.
325
+ * Files already on disk keep their local-time names; the runner reads any 14
326
+ * digits.
244
327
  */
245
328
  export function formatTimestamp(date) {
246
329
  return [
247
- date.getFullYear(),
248
- String(date.getMonth() + 1).padStart(2, '0'),
249
- String(date.getDate()).padStart(2, '0'),
250
- String(date.getHours()).padStart(2, '0'),
251
- String(date.getMinutes()).padStart(2, '0'),
252
- String(date.getSeconds()).padStart(2, '0'),
330
+ date.getUTCFullYear(),
331
+ String(date.getUTCMonth() + 1).padStart(2, '0'),
332
+ String(date.getUTCDate()).padStart(2, '0'),
333
+ String(date.getUTCHours()).padStart(2, '0'),
334
+ String(date.getUTCMinutes()).padStart(2, '0'),
335
+ String(date.getUTCSeconds()).padStart(2, '0'),
253
336
  ].join('');
254
337
  }
255
338
  /**
@@ -282,16 +365,20 @@ export function listMigrationFiles(migrationsDir) {
282
365
  /**
283
366
  * `.sql` files in `migrationsDir` that the runner NEVER sees, because their
284
367
  * name lacks the `YYYYMMDDHHMMSS_<name>.sql` shape {@link listMigrationFiles}
285
- * requires (a hand-written `add_thing.sql`, a 13-digit prefix). Such a file
286
- * used to be skipped in silence by `status`, `up` and `deploy` alike, so a
287
- * misnamed migration simply never ran and nothing said so. The CLI prints one
288
- * warning per entry.
368
+ * requires (a hand-written `add_thing.sql`, a 13-digit prefix, an upper-case
369
+ * `.SQL` extension). Such a file used to be skipped in silence by `status`,
370
+ * `up` and `deploy` alike, so a misnamed migration simply never ran and
371
+ * nothing said so. The CLI prints one warning per entry.
372
+ *
373
+ * The extension is matched in any case here and only in lower case by the
374
+ * runner: `20260101000000_init.SQL` is a migration somebody meant to run, so
375
+ * it is reported rather than ignored like a `.txt` file.
289
376
  */
290
377
  export function listIgnoredSqlFiles(migrationsDir) {
291
378
  if (!existsSync(migrationsDir))
292
379
  return [];
293
380
  return readdirSync(migrationsDir)
294
- .filter((f) => f.endsWith('.sql') && parseMigrationFilename(f) === null)
381
+ .filter((f) => /\.sql$/i.test(f) && parseMigrationFilename(f) === null)
295
382
  .sort();
296
383
  }
297
384
  /**
@@ -540,19 +627,50 @@ const DESTRUCTIVE_MIGRATION_BANNER = [
540
627
  '-- every flagged statement carefully before running.',
541
628
  '-- ============================================================',
542
629
  ];
630
+ /** Same banner when only the DOWN direction is destructive: UP runs without a prompt. */
631
+ const DESTRUCTIVE_DOWN_ONLY_BANNER = [
632
+ '-- ============================================================',
633
+ '-- WARNING: this migration contains DESTRUCTIVE statement(s).',
634
+ '-- Only the DOWN section is destructive (flagged inline there).',
635
+ '-- `turbine migrate up` runs UP as written; `turbine migrate down`',
636
+ '-- asks you to confirm before it runs the flagged statements.',
637
+ '-- ============================================================',
638
+ ];
639
+ /** True for a block that is nothing but comments and blank lines. */
640
+ function isCommentBlock(text) {
641
+ return text.split('\n').every((line) => {
642
+ const t = line.trim();
643
+ return t === '' || t.startsWith('--');
644
+ });
645
+ }
543
646
  /**
544
647
  * Annotate a list of SQL statements: scan each for data-destroying operations
545
648
  * (via {@link scanDestructiveSql}) and prefix any offender with loud, commented
546
649
  * warnings. Statements are left intact so the existing `migrate up` gate still
547
650
  * refuses them by default; the comments just make the danger visible on review.
651
+ *
652
+ * A comment-only entry (the DOWN placeholder for a dropped column) is written
653
+ * through untouched: it is not SQL, so it is neither scanned nor given a `;`.
654
+ * A statement the diff proved to be a lossless widening gets its
655
+ * {@link wideningMarker} instead of a warning, and is not scanned.
548
656
  */
549
- function annotateDiffStatements(statements) {
657
+ function annotateDiffStatements(statements, widenings = []) {
550
658
  const destructive = [];
551
659
  const out = [];
660
+ const widened = new Map(widenings.map((w) => [w.statement.trim(), w]));
552
661
  for (const raw of statements) {
553
662
  const stmt = raw.trim();
554
663
  if (!stmt)
555
664
  continue;
665
+ if (isCommentBlock(stmt)) {
666
+ out.push(stmt);
667
+ continue;
668
+ }
669
+ const widening = widened.get(stmt.endsWith(';') ? stmt : `${stmt};`);
670
+ if (widening) {
671
+ out.push(wideningMarker(widening), stmt.endsWith(';') ? stmt : `${stmt};`);
672
+ continue;
673
+ }
556
674
  const hits = scanDestructiveSql(stmt);
557
675
  if (hits.length > 0) {
558
676
  destructive.push(...hits);
@@ -575,18 +693,23 @@ function annotateDiffStatements(statements) {
575
693
  * when any exist, a loud file-level banner is prepended to UP.
576
694
  * - Any diff `warnings` (changes the diff refuses to apply automatically, e.g.
577
695
  * enum value removals) are surfaced as `-- NOTE:` comments in UP.
696
+ * - A type change the diff proved lossless (`widenings`) is not flagged: it is
697
+ * preceded by a {@link wideningMarker}, which `migrate up` checks before it
698
+ * stops excusing the statement.
699
+ * - Every `DROP COLUMN` in UP has a comment block in DOWN saying the data
700
+ * cannot be restored automatically (the diff supplies it as a reverse entry).
578
701
  *
579
702
  * Pure and DB-free, so it is unit-testable from a synthesized diff.
580
703
  */
581
704
  export function buildDiffMigrationBody(diff) {
582
- const up = annotateDiffStatements(diff.statements);
705
+ const up = annotateDiffStatements(diff.statements, diff.widenings);
583
706
  const hasReverse = diff.reverseStatements.length > 0;
584
707
  const down = hasReverse
585
708
  ? annotateDiffStatements(diff.reverseStatements)
586
709
  : { text: '', destructive: [] };
587
710
  const upParts = [];
588
711
  if (up.destructive.length > 0 || down.destructive.length > 0) {
589
- upParts.push(...DESTRUCTIVE_MIGRATION_BANNER, '');
712
+ upParts.push(...(up.destructive.length > 0 ? DESTRUCTIVE_MIGRATION_BANNER : DESTRUCTIVE_DOWN_ONLY_BANNER), '');
590
713
  }
591
714
  if (diff.warnings && diff.warnings.length > 0) {
592
715
  for (const w of diff.warnings)
@@ -676,7 +799,18 @@ ${autoContent.down}
676
799
  -- Write your rollback SQL here
677
800
  `;
678
801
  }
679
- writeFileSync(filePath, template, 'utf-8');
802
+ // `wx` (create, fail if the name exists): the name carries a one-second
803
+ // timestamp, so a second create with the same name in that second would
804
+ // otherwise silently replace the first file, edits included.
805
+ try {
806
+ writeFileSync(filePath, template, { encoding: 'utf-8', flag: 'wx' });
807
+ }
808
+ catch (err) {
809
+ if (err.code !== 'EEXIST')
810
+ throw err;
811
+ throw new MigrationError(`Migration file ${filename} already exists, and its name is timestamped to the second. ` +
812
+ 'Wait a second and run the command again, or choose a different name.');
813
+ }
680
814
  return {
681
815
  filename,
682
816
  path: filePath,
@@ -921,6 +1055,33 @@ export async function acquireMigrationLock(runner, lockId, adapter, openLockConn
921
1055
  throw err;
922
1056
  }
923
1057
  }
1058
+ /**
1059
+ * {@link acquireMigrationLock}, retried until `wait.timeoutMs` has passed.
1060
+ *
1061
+ * Polls the adapter's own non-blocking acquire rather than issuing a blocking
1062
+ * `pg_advisory_lock`: a blocking wait needs a `lock_timeout` to be bounded, and
1063
+ * setting one is a `SET`, which must never run on a connection that may sit
1064
+ * behind a pooler. Polling needs nothing but the statement the adapter already
1065
+ * uses, and works for the lock-table adapters too. Without `wait` it is exactly
1066
+ * one attempt, which is what every caller did before.
1067
+ *
1068
+ * @internal exported for tests.
1069
+ */
1070
+ export async function acquireMigrationLockWaiting(runner, lockId, adapter, openLockConnection, wait) {
1071
+ let lock = await acquireMigrationLock(runner, lockId, adapter, openLockConnection);
1072
+ if (lock.acquired || !wait)
1073
+ return lock;
1074
+ const now = wait.now ?? Date.now;
1075
+ const sleep = wait.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1076
+ const interval = wait.intervalMs ?? 1000;
1077
+ const deadline = now() + wait.timeoutMs;
1078
+ wait.onWait?.();
1079
+ while (!lock.acquired && now() < deadline) {
1080
+ await sleep(Math.max(0, Math.min(interval, deadline - now())));
1081
+ lock = await acquireMigrationLock(runner, lockId, adapter, openLockConnection);
1082
+ }
1083
+ return lock;
1084
+ }
924
1085
  /**
925
1086
  * Release a lock taken by {@link acquireMigrationLock}, and close the dedicated
926
1087
  * connection when there is one. A refused lock owns nothing, so releasing it is
@@ -1147,14 +1308,29 @@ action = 'apply pending migrations') {
1147
1308
  export function assertUpHasStatements(files) {
1148
1309
  for (const file of files) {
1149
1310
  const { up } = parseMigrationSQL(file.path);
1150
- const executable = tokenizeSql(up).filter((s) => !s.commentOnly && s.stripped.trim() !== '');
1151
- if (executable.length > 0)
1311
+ if (hasExecutableStatement(up))
1152
1312
  continue;
1153
1313
  throw new MigrationError(`Migration ${file.filename} has an UP section with no executable statement (comments only). ` +
1154
1314
  `Applying it would record the migration as applied while running nothing, and filling the file in ` +
1155
1315
  `afterwards would then be refused as drift. Write the UP section or delete the file.`);
1156
1316
  }
1157
1317
  }
1318
+ /** True when `sql` holds at least one statement that is not just comments. */
1319
+ export function hasExecutableStatement(sql) {
1320
+ return tokenizeSql(sql).some((s) => !s.commentOnly && s.stripped.trim() !== '');
1321
+ }
1322
+ /**
1323
+ * The files among `files` whose DOWN section runs no SQL at all (comments only).
1324
+ *
1325
+ * A diff-generated migration that drops a column ships a commented IRREVERSIBLE
1326
+ * note as its DOWN, because no statement can bring the data back. Rolling such a
1327
+ * migration back succeeds, removes its tracking row, and restores nothing, so
1328
+ * the CLI names these files after a rollback instead of letting "rolled back"
1329
+ * stand unqualified.
1330
+ */
1331
+ export function rollbacksWithoutSql(files) {
1332
+ return files.filter((f) => f.path !== '' && !hasExecutableStatement(parseMigrationSQL(f.path).down));
1333
+ }
1158
1334
  /**
1159
1335
  * Build a deploy plan from local migration files and applied migration rows.
1160
1336
  * This is pure file-system planning; callers with a database connection should
@@ -1240,9 +1416,13 @@ export async function migrateUp(connectionString, migrationsDir, options) {
1240
1416
  // table-lock adapter gets its OWN connection so the per-migration
1241
1417
  // BEGIN/COMMIT below cannot end the transaction the lock lives in.
1242
1418
  const adapter = options?.adapter;
1243
- const lock = await acquireMigrationLock(client, lockId, adapter, () => openLockConnection(url));
1419
+ const lockWaitMs = options?.lockWaitMs;
1420
+ const lock = await acquireMigrationLockWaiting(client, lockId, adapter, () => openLockConnection(url), lockWaitMs !== undefined && lockWaitMs > 0 ? { timeoutMs: lockWaitMs, onWait: options?.onLockWait } : undefined);
1244
1421
  if (!lock.acquired) {
1245
- throw new MigrationError('Could not acquire migration lock, another migration is already running');
1422
+ throw new MigrationError(lockWaitMs !== undefined && lockWaitMs > 0
1423
+ ? `Could not acquire migration lock within ${Math.round(lockWaitMs / 1000)}s: another migration is still running. ` +
1424
+ 'Raise --wait if it is expected to take longer.'
1425
+ : 'Could not acquire migration lock, another migration is already running');
1246
1426
  }
1247
1427
  try {
1248
1428
  await ensureTrackingTable(client, dialect);
@@ -1384,6 +1564,8 @@ export async function migrateDeploy(connectionString, migrationsDir, options) {
1384
1564
  allowDestructive: true,
1385
1565
  adapter: options?.adapter,
1386
1566
  schema: options?.schema,
1567
+ lockWaitMs: options?.lockWaitMs,
1568
+ onLockWait: options?.onLockWait,
1387
1569
  });
1388
1570
  }
1389
1571
  /**
@@ -1652,3 +1834,61 @@ export async function migrateStatus(connectionString, migrationsDir, options) {
1652
1834
  await client.end();
1653
1835
  }
1654
1836
  }
1837
+ // ---------------------------------------------------------------------------
1838
+ // Status helpers (pending files, CI exit code)
1839
+ // ---------------------------------------------------------------------------
1840
+ /** The migration files in `statuses` that have not been applied to the database. */
1841
+ export function pendingMigrationFiles(statuses) {
1842
+ return statuses.filter((st) => !st.applied).map((st) => st.file);
1843
+ }
1844
+ /**
1845
+ * The refusal `migrate create --auto` / `--from-diff` prints while migrations
1846
+ * are still pending.
1847
+ *
1848
+ * Both flags diff the schema file against the LIVE database. A pending migration
1849
+ * is a change the file already describes and the database does not have yet, so
1850
+ * the diff reports it AGAIN: the new migration repeats those statements and
1851
+ * fails on the first database that applies the pending one before it.
1852
+ */
1853
+ export function formatPendingAutoRefusal(pending, flag) {
1854
+ const lines = [
1855
+ `Refusing to create a migration with ${flag}: ${pending.length} migration(s) in the migrations directory are not applied to this database yet:`,
1856
+ '',
1857
+ ...pending.map((f) => ` ${f.filename}`),
1858
+ '',
1859
+ `${flag} compares your schema file with the LIVE database. The pending file(s) above already describe part of`,
1860
+ 'that difference, so the new migration would repeat their statements and fail when they run first.',
1861
+ '',
1862
+ 'Apply them, then run this command again: npx turbine migrate up',
1863
+ `If they do not touch what the schema file describes (a data migration, say), pass --allow-pending.`,
1864
+ ];
1865
+ return lines.join('\n');
1866
+ }
1867
+ /**
1868
+ * Every reason `migrate status --exit-code` exits 2, as phrases for the message.
1869
+ * Empty means the gate passes. `ignoredFiles` are the `.sql` files in the
1870
+ * migrations directory whose name the runner never matches (see
1871
+ * {@link listIgnoredSqlFiles}): a migration that will never run is as much a
1872
+ * disagreement between the files and the database as a pending one.
1873
+ */
1874
+ export function statusGateProblems(health, ignoredFiles) {
1875
+ const problems = [];
1876
+ if (health.pending > 0)
1877
+ problems.push(`${health.pending} pending`);
1878
+ if (health.drifted > 0)
1879
+ problems.push(`${health.drifted} modified after being applied`);
1880
+ if (health.missing > 0)
1881
+ problems.push(`${health.missing} missing from disk`);
1882
+ if (ignoredFiles.length > 0) {
1883
+ problems.push(`${ignoredFiles.length} misnamed .sql file(s) that will never run (${ignoredFiles.join(', ')}; ` +
1884
+ `a migration filename is YYYYMMDDHHMMSS_name.sql)`);
1885
+ }
1886
+ return problems;
1887
+ }
1888
+ /** Count the ways a migration history can disagree with the files on disk. */
1889
+ export function summarizeMigrationStatus(statuses) {
1890
+ const pending = statuses.filter((st) => !st.applied).length;
1891
+ const missing = statuses.filter((st) => st.missingFile).length;
1892
+ const drifted = statuses.filter((st) => st.applied && st.checksumValid === false && !st.missingFile).length;
1893
+ return { pending, drifted, missing, ok: pending === 0 && drifted === 0 && missing === 0 };
1894
+ }
@@ -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>;