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.
- package/README.md +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -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
|
|
185
|
-
* used to be skipped in silence by `status`,
|
|
186
|
-
* misnamed migration simply never ran and
|
|
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;
|
package/dist/cli/migrate.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
248
|
-
String(date.
|
|
249
|
-
String(date.
|
|
250
|
-
String(date.
|
|
251
|
-
String(date.
|
|
252
|
-
String(date.
|
|
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
|
|
286
|
-
* used to be skipped in silence by `status`,
|
|
287
|
-
* misnamed migration simply never ran and
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
+
}
|
package/dist/cli/observe.d.ts
CHANGED
|
@@ -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
|
-
}
|
|
28
|
+
}>,
|
|
29
|
+
/** The one-time login ticket. Absent on a hand-built call: the ticket is then `authToken` itself. */
|
|
30
|
+
ticketGate?: TicketGate): Promise<void>;
|