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