turbine-orm 0.35.0 → 0.36.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 +18 -16
- package/dist/cjs/cli/index.js +109 -16
- package/dist/cjs/cli/migrate.js +78 -3
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +333 -22
- package/dist/cjs/cli/ui.js +7 -1
- package/dist/cjs/dialect.js +1 -1
- package/dist/cjs/generate.js +23 -2
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/mssql.js +22 -5
- package/dist/cjs/powdb.js +41 -1
- package/dist/cjs/powql.js +80 -25
- package/dist/cjs/query/aggregates.js +683 -0
- package/dist/cjs/query/batched-loader.js +2 -0
- package/dist/cjs/query/builder.js +297 -4504
- package/dist/cjs/query/filters.js +12 -0
- package/dist/cjs/query/relations.js +1698 -0
- package/dist/cjs/query/where-compile.js +180 -0
- package/dist/cjs/query/where.js +1491 -0
- package/dist/cjs/query/writes.js +680 -0
- package/dist/cjs/schema-builder.js +6 -0
- package/dist/cjs/schema-metadata.js +4 -0
- package/dist/cjs/schema-sql.js +265 -3
- package/dist/cjs/sqlite.js +1 -1
- package/dist/cli/index.d.ts +8 -2
- package/dist/cli/index.js +111 -18
- package/dist/cli/migrate.d.ts +24 -1
- package/dist/cli/migrate.js +77 -3
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +46 -13
- package/dist/cli/studio.js +331 -23
- package/dist/cli/ui.js +7 -1
- package/dist/dialect.d.ts +15 -6
- package/dist/dialect.js +1 -1
- package/dist/generate.js +23 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +22 -5
- package/dist/powdb.d.ts +20 -0
- package/dist/powdb.js +40 -0
- package/dist/powql.d.ts +33 -1
- package/dist/powql.js +80 -25
- package/dist/query/aggregates.d.ts +74 -0
- package/dist/query/aggregates.js +641 -0
- package/dist/query/batched-loader.d.ts +6 -0
- package/dist/query/batched-loader.js +2 -0
- package/dist/query/builder.d.ts +62 -829
- package/dist/query/builder.js +302 -4509
- package/dist/query/deferred.d.ts +7 -0
- package/dist/query/filters.d.ts +7 -0
- package/dist/query/filters.js +11 -0
- package/dist/query/relations.d.ts +441 -0
- package/dist/query/relations.js +1627 -0
- package/dist/query/types.d.ts +15 -0
- package/dist/query/where-compile.d.ts +139 -0
- package/dist/query/where-compile.js +175 -0
- package/dist/query/where.d.ts +494 -0
- package/dist/query/where.js +1431 -0
- package/dist/query/writes.d.ts +131 -0
- package/dist/query/writes.js +626 -0
- package/dist/schema-builder.d.ts +18 -3
- package/dist/schema-builder.js +6 -0
- package/dist/schema-metadata.js +4 -0
- package/dist/schema-sql.d.ts +60 -3
- package/dist/schema-sql.js +261 -4
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +1 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Commands:
|
|
6
6
|
* turbine init — Initialize a Turbine project
|
|
7
7
|
* turbine generate | pull — Introspect database and generate TypeScript types
|
|
8
|
-
* turbine push
|
|
9
|
-
* turbine migrate create <name>
|
|
8
|
+
* turbine push - Apply schema-builder definitions to database (destructive ops gated)
|
|
9
|
+
* turbine migrate create <name> - Create a new SQL migration file (--auto | --recipe <name>)
|
|
10
10
|
* turbine migrate up — Apply pending migrations
|
|
11
11
|
* turbine migrate deploy — Apply pending migrations without prompts
|
|
12
12
|
* turbine migrate down — Rollback last migration
|
|
@@ -29,11 +29,11 @@ import { pathToFileURL } from 'node:url';
|
|
|
29
29
|
import { generate } from '../generate.js';
|
|
30
30
|
import { findMissingRelationIndexes } from '../index-advisor.js';
|
|
31
31
|
import { introspect } from '../introspect.js';
|
|
32
|
-
import { schemaDiff, schemaPush } from '../schema-sql.js';
|
|
32
|
+
import { DestructivePushRefusal, schemaDiff, schemaPush } from '../schema-sql.js';
|
|
33
33
|
import { configTemplate, findConfigFile, loadConfigResult, looksLikeSchemaFilePath, resolveConfig, resolveSeedFile, unwrapModuleDefault, } from './config.js';
|
|
34
34
|
import { canResolveTsx, getTsLoaderError, needsTsLoader, registerTsLoader } from './loader.js';
|
|
35
35
|
import { runMcpServer } from './mcp.js';
|
|
36
|
-
import { createMigration, inspectMigrationDeploy, listMigrationFiles, migrateDeploy, migrateDown, migrateStatus, migrateUp, } from './migrate.js';
|
|
36
|
+
import { createMigration, inspectMigrationDeploy, listMigrationFiles, MIGRATION_RECIPES, migrateDeploy, migrateDown, migrateStatus, migrateUp, } from './migrate.js';
|
|
37
37
|
import { startObserve } from './observe.js';
|
|
38
38
|
import { startStudio } from './studio.js';
|
|
39
39
|
import { banner, blue, bold, box, cyan, dim, divider, elapsed, error, table as formatTable, gray, green, header, info, label, magenta, newline, red, redactUrl, Spinner, success, symbols, warn, yellow, } from './ui.js';
|
|
@@ -108,6 +108,14 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
108
108
|
case '--allow-destructive':
|
|
109
109
|
result.allowDestructive = true;
|
|
110
110
|
break;
|
|
111
|
+
case '--recipe':
|
|
112
|
+
if (next === undefined || next.startsWith('-')) {
|
|
113
|
+
console.error('--recipe requires a name (e.g. --recipe backfill)');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
result.recipe = next;
|
|
117
|
+
i++;
|
|
118
|
+
break;
|
|
111
119
|
case '--force':
|
|
112
120
|
case '-f':
|
|
113
121
|
result.force = true;
|
|
@@ -134,6 +142,12 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
134
142
|
case '--allow-remote':
|
|
135
143
|
result.allowRemote = true;
|
|
136
144
|
break;
|
|
145
|
+
case '--write':
|
|
146
|
+
result.write = true;
|
|
147
|
+
break;
|
|
148
|
+
case '--show-pii':
|
|
149
|
+
result.showPii = true;
|
|
150
|
+
break;
|
|
137
151
|
default:
|
|
138
152
|
if (!arg.startsWith('-')) {
|
|
139
153
|
result.positional.push(arg);
|
|
@@ -731,14 +745,45 @@ async function cmdPush(args, config) {
|
|
|
731
745
|
}
|
|
732
746
|
newline();
|
|
733
747
|
}
|
|
748
|
+
// Surface any non-fatal diff warnings (e.g. undeclared DB indexes, enum
|
|
749
|
+
// removals) the diff refuses to apply automatically.
|
|
750
|
+
if (diff.warnings && diff.warnings.length > 0) {
|
|
751
|
+
for (const w of diff.warnings)
|
|
752
|
+
warn(w);
|
|
753
|
+
newline();
|
|
754
|
+
}
|
|
734
755
|
if (args.dryRun) {
|
|
735
756
|
info('Dry run — no changes applied.');
|
|
736
757
|
newline();
|
|
737
758
|
return;
|
|
738
759
|
}
|
|
739
|
-
|
|
760
|
+
if (args.allowDestructive) {
|
|
761
|
+
warn('--allow-destructive is set: data-destroying schema changes WILL run.');
|
|
762
|
+
newline();
|
|
763
|
+
}
|
|
764
|
+
// Execute (gated): schemaPush throws on destructive statements unless allowed.
|
|
765
|
+
// Pass the diff computed above as `precomputedDiff` so schemaPush applies the
|
|
766
|
+
// EXACT statements just displayed and confirmed, with no re-diff between confirm
|
|
767
|
+
// and apply (the TOCTOU window where a concurrent schema change could alter
|
|
768
|
+
// the applied set). Both the initial attempt and the post-confirmation retry
|
|
769
|
+
// reuse `diff`, so the confirmed plan and the applied plan are identical.
|
|
740
770
|
const pushSpinner = new Spinner('Applying changes').start();
|
|
741
|
-
|
|
771
|
+
let result;
|
|
772
|
+
try {
|
|
773
|
+
result = await schemaPush(schemaDef, url, { allowDestructive: args.allowDestructive, precomputedDiff: diff });
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
if (!(err instanceof DestructivePushRefusal))
|
|
777
|
+
throw err;
|
|
778
|
+
pushSpinner.stop();
|
|
779
|
+
if (!(await confirmDestructive(err.message))) {
|
|
780
|
+
error('Aborted: no changes were applied and no data was touched.');
|
|
781
|
+
newline();
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
pushSpinner.start();
|
|
785
|
+
result = await schemaPush(schemaDef, url, { allowDestructive: true, precomputedDiff: diff });
|
|
786
|
+
}
|
|
742
787
|
pushSpinner.succeed(`Applied ${bold(String(result.statementsExecuted))} statement(s)`);
|
|
743
788
|
if (result.tablesCreated.length > 0) {
|
|
744
789
|
success(`Created: ${result.tablesCreated.join(', ')}`);
|
|
@@ -760,22 +805,30 @@ async function cmdMigrate(args, config) {
|
|
|
760
805
|
console.log(` ${bold('turbine migrate')} ${dim('— SQL-first migration system')}`);
|
|
761
806
|
newline();
|
|
762
807
|
console.log(` ${bold('Commands:')}`);
|
|
763
|
-
console.log(` ${cyan('create <name>')}
|
|
764
|
-
console.log(` ${cyan('create <name> --auto')}
|
|
765
|
-
console.log(` ${cyan('
|
|
766
|
-
console.log(` ${cyan('
|
|
767
|
-
console.log(` ${cyan('
|
|
768
|
-
console.log(` ${cyan('
|
|
808
|
+
console.log(` ${cyan('create <name>')} Create a new migration file`);
|
|
809
|
+
console.log(` ${cyan('create <name> --auto')} Auto-generate from schema diff`);
|
|
810
|
+
console.log(` ${cyan('create <name> --recipe')} Scaffold a named recipe (e.g. backfill)`);
|
|
811
|
+
console.log(` ${cyan('up')} Apply pending migrations`);
|
|
812
|
+
console.log(` ${cyan('deploy')} Apply pending migrations without prompts`);
|
|
813
|
+
console.log(` ${cyan('down')} Rollback last migration`);
|
|
814
|
+
console.log(` ${cyan('status')} Show migration status`);
|
|
769
815
|
newline();
|
|
770
816
|
console.log(` ${bold('Options:')}`);
|
|
771
|
-
console.log(` ${cyan('--auto')}
|
|
772
|
-
console.log(` ${cyan('--
|
|
773
|
-
console.log(` ${cyan('--
|
|
774
|
-
console.log(` ${cyan('--
|
|
817
|
+
console.log(` ${cyan('--auto')} Auto-generate UP/DOWN SQL from schema diff`);
|
|
818
|
+
console.log(` ${cyan('--recipe <name>')} Scaffold a sanctioned migration pattern`);
|
|
819
|
+
console.log(` ${cyan('--step, -n')} Number of migrations to apply/rollback`);
|
|
820
|
+
console.log(` ${cyan('--dry-run')} Show SQL without executing`);
|
|
821
|
+
console.log(` ${cyan('--allow-drift')} Bypass checksum validation on ${cyan('migrate up')} ${dim('(advanced)')}`);
|
|
822
|
+
newline();
|
|
823
|
+
console.log(` ${bold('Recipes')} ${dim('(--recipe):')}`);
|
|
824
|
+
for (const [key, recipe] of Object.entries(MIGRATION_RECIPES)) {
|
|
825
|
+
console.log(` ${cyan(key)} ${dim(recipe.description)}`);
|
|
826
|
+
}
|
|
775
827
|
newline();
|
|
776
828
|
console.log(` ${bold('Examples:')}`);
|
|
777
829
|
console.log(` ${dim('npx turbine migrate create add_users_table')}`);
|
|
778
830
|
console.log(` ${dim('npx turbine migrate create add_email_index --auto')}`);
|
|
831
|
+
console.log(` ${dim('npx turbine migrate create backfill_full_name --recipe backfill')}`);
|
|
779
832
|
console.log(` ${dim('npx turbine migrate up')}`);
|
|
780
833
|
console.log(` ${dim('npx turbine migrate deploy --dry-run')}`);
|
|
781
834
|
console.log(` ${dim('npx turbine migrate down --step 2')}`);
|
|
@@ -868,6 +921,28 @@ async function cmdMigrateCreate(args, config) {
|
|
|
868
921
|
newline();
|
|
869
922
|
return;
|
|
870
923
|
}
|
|
924
|
+
if (args.recipe) {
|
|
925
|
+
if (!MIGRATION_RECIPES[args.recipe]) {
|
|
926
|
+
error(`Unknown migration recipe: ${args.recipe}`);
|
|
927
|
+
newline();
|
|
928
|
+
console.log(` ${dim('Available recipes:')}`);
|
|
929
|
+
for (const [key, recipe] of Object.entries(MIGRATION_RECIPES)) {
|
|
930
|
+
console.log(` ${cyan(key)} ${dim(recipe.description)}`);
|
|
931
|
+
}
|
|
932
|
+
newline();
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
const file = createMigration(config.migrationsDir, name, undefined, { recipe: args.recipe });
|
|
936
|
+
const relPath = relative(process.cwd(), file.path);
|
|
937
|
+
success(`Created ${args.recipe} migration: ${bold(file.filename)}`);
|
|
938
|
+
newline();
|
|
939
|
+
console.log(` ${dim('File:')} ${cyan(relPath)}`);
|
|
940
|
+
newline();
|
|
941
|
+
console.log(` ${dim('Fill in the commented placeholders, then run:')}`);
|
|
942
|
+
console.log(` ${cyan('npx turbine migrate up')}`);
|
|
943
|
+
newline();
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
871
946
|
const file = createMigration(config.migrationsDir, name);
|
|
872
947
|
const relPath = relative(process.cwd(), file.path);
|
|
873
948
|
success(`Created migration: ${bold(file.filename)}`);
|
|
@@ -1420,6 +1495,8 @@ async function cmdStudio(args, config) {
|
|
|
1420
1495
|
openBrowser,
|
|
1421
1496
|
include: config.include.length ? config.include : undefined,
|
|
1422
1497
|
exclude: config.exclude.length ? config.exclude : undefined,
|
|
1498
|
+
write: args.write === true,
|
|
1499
|
+
showPii: args.showPii === true,
|
|
1423
1500
|
});
|
|
1424
1501
|
spinner.succeed(`Studio is running`);
|
|
1425
1502
|
}
|
|
@@ -1427,13 +1504,24 @@ async function cmdStudio(args, config) {
|
|
|
1427
1504
|
spinner.fail(`Failed to start Studio: ${err instanceof Error ? err.message : String(err)}`);
|
|
1428
1505
|
process.exit(1);
|
|
1429
1506
|
}
|
|
1507
|
+
// Loud startup warnings for the opt-in modes that widen Studio's surface.
|
|
1508
|
+
if (args.write) {
|
|
1509
|
+
newline();
|
|
1510
|
+
console.log(warn('WRITE MODE is ON. Studio can update, insert, and delete single rows in ' +
|
|
1511
|
+
`${redactUrl(url)}. Every change is committed directly to your database.`));
|
|
1512
|
+
}
|
|
1513
|
+
if (args.showPii) {
|
|
1514
|
+
newline();
|
|
1515
|
+
console.log(warn('--show-pii is ON. PII-tagged column values are shown UNREDACTED in Studio.'));
|
|
1516
|
+
}
|
|
1430
1517
|
newline();
|
|
1431
1518
|
console.log(box([
|
|
1432
|
-
`${bold('Turbine Studio')} ${dim('
|
|
1519
|
+
`${bold('Turbine Studio')} ${dim(args.write ? 'local UI (WRITE MODE)' : 'local read-only UI')}`,
|
|
1433
1520
|
'',
|
|
1434
1521
|
` ${cyan('URL:')} ${bold(studio.url)}`,
|
|
1435
1522
|
` ${cyan('Schema:')} ${config.schema}`,
|
|
1436
1523
|
` ${cyan('DB:')} ${redactUrl(url)}`,
|
|
1524
|
+
` ${cyan('Mode:')} ${args.write ? red('read-write (single-row)') : 'read-only'}`,
|
|
1437
1525
|
'',
|
|
1438
1526
|
dim('Open the URL above in your browser. It includes a one-time session'),
|
|
1439
1527
|
dim('token that gets set as an HttpOnly cookie on first load.'),
|
|
@@ -1613,6 +1701,7 @@ function showPushHelp() {
|
|
|
1613
1701
|
console.log(` ${bold('Options:')}`);
|
|
1614
1702
|
console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
|
|
1615
1703
|
console.log(` ${cyan('--dry-run')} Show SQL without executing`);
|
|
1704
|
+
console.log(` ${cyan('--allow-destructive')} Skip the interactive confirmation for data-destroying statements ${dim('(CI)')}`);
|
|
1616
1705
|
console.log(` ${cyan('--verbose, -v')} Show detailed output`);
|
|
1617
1706
|
newline();
|
|
1618
1707
|
}
|
|
@@ -1633,6 +1722,7 @@ function showMigrateHelp() {
|
|
|
1633
1722
|
console.log(` ${bold('Options:')}`);
|
|
1634
1723
|
console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
|
|
1635
1724
|
console.log(` ${cyan('--auto')} Auto-generate UP/DOWN SQL from schema diff ${dim('(create only)')}`);
|
|
1725
|
+
console.log(` ${cyan('--recipe')} ${dim('<name>')} Scaffold a sanctioned migration pattern ${dim('(create only, e.g. backfill)')}`);
|
|
1636
1726
|
console.log(` ${cyan('--step, -n')} ${dim('<N>')} Number of migrations to apply/rollback`);
|
|
1637
1727
|
console.log(` ${cyan('--dry-run')} Show SQL without executing`);
|
|
1638
1728
|
console.log(` ${cyan('--allow-drift')} Bypass checksum validation ${dim('(migrate up only — advanced)')}`);
|
|
@@ -1642,6 +1732,7 @@ function showMigrateHelp() {
|
|
|
1642
1732
|
console.log(` ${bold('Examples:')}`);
|
|
1643
1733
|
console.log(` ${dim('$')} npx turbine migrate create add_users_table`);
|
|
1644
1734
|
console.log(` ${dim('$')} npx turbine migrate create add_email_index --auto`);
|
|
1735
|
+
console.log(` ${dim('$')} npx turbine migrate create backfill_full_name --recipe backfill`);
|
|
1645
1736
|
console.log(` ${dim('$')} npx turbine migrate up`);
|
|
1646
1737
|
console.log(` ${dim('$')} npx turbine migrate deploy --dry-run`);
|
|
1647
1738
|
console.log(` ${dim('$')} npx turbine migrate down --step 2`);
|
|
@@ -1717,7 +1808,7 @@ function showHelp() {
|
|
|
1717
1808
|
console.log(` ${cyan('seed')} Run seed file`);
|
|
1718
1809
|
console.log(` ${cyan('status')} ${dim('| info')} Show schema summary`);
|
|
1719
1810
|
console.log(` ${cyan('doctor')} Check relations for missing FK indexes ${dim('(--fix emits migration)')}`);
|
|
1720
|
-
console.log(` ${cyan('studio')} Launch local read-only web UI`);
|
|
1811
|
+
console.log(` ${cyan('studio')} Launch local read-only web UI ${dim('(--write opts in to single-row writes)')}`);
|
|
1721
1812
|
console.log(` ${cyan('mcp')} Start read-only MCP server over stdio`);
|
|
1722
1813
|
console.log(` ${cyan('observe')} Launch metrics dashboard ${dim('(requires TURBINE_OBSERVE_URL)')}`);
|
|
1723
1814
|
newline();
|
|
@@ -1741,6 +1832,8 @@ function showHelp() {
|
|
|
1741
1832
|
console.log(` ${cyan('--host')} ${dim('<addr>')} Bind address ${dim('(default: 127.0.0.1)')}`);
|
|
1742
1833
|
console.log(` ${cyan('--no-open')} Don't auto-open the browser`);
|
|
1743
1834
|
console.log(` ${cyan('--allow-remote')} Allow non-loopback --host ${dim('(refused without this flag)')}`);
|
|
1835
|
+
console.log(` ${cyan('--write')} Studio: enable single-row update/insert/delete ${dim('(read-only by default)')}`);
|
|
1836
|
+
console.log(` ${cyan('--show-pii')} Studio: show PII-tagged values unredacted ${dim('(redacted by default)')}`);
|
|
1744
1837
|
newline();
|
|
1745
1838
|
console.log(` ${bold('Config file:')}`);
|
|
1746
1839
|
console.log(` ${dim('Create')} ${cyan('turbine.config.ts')} ${dim('with')} ${cyan('npx turbine init')}`);
|
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -73,13 +73,36 @@ export declare function parseMigrationSQL(filePath: string): {
|
|
|
73
73
|
up: string;
|
|
74
74
|
down: string;
|
|
75
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* A named migration scaffold. `build()` returns the commented-SQL UP/DOWN body
|
|
78
|
+
* for the recipe; `createMigration({ recipe })` wraps it in the file header.
|
|
79
|
+
*/
|
|
80
|
+
export interface MigrationRecipe {
|
|
81
|
+
/** One-line description shown in CLI help. */
|
|
82
|
+
description: string;
|
|
83
|
+
/** Build the UP/DOWN body (commented scaffold with placeholders). */
|
|
84
|
+
build(name: string): {
|
|
85
|
+
up: string;
|
|
86
|
+
down: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Registry of migration recipes, keyed by `--recipe <name>`. New recipes slot
|
|
91
|
+
* in here without touching {@link createMigration} or the CLI handler.
|
|
92
|
+
*/
|
|
93
|
+
export declare const MIGRATION_RECIPES: Record<string, MigrationRecipe>;
|
|
76
94
|
/**
|
|
77
95
|
* Create a new migration file.
|
|
78
|
-
*
|
|
96
|
+
*
|
|
97
|
+
* - `autoContent`: pre-populate UP/DOWN from a schema diff.
|
|
98
|
+
* - `options.recipe`: scaffold a named recipe (see {@link MIGRATION_RECIPES}).
|
|
99
|
+
* Mutually exclusive with `autoContent`; an unknown recipe throws.
|
|
79
100
|
*/
|
|
80
101
|
export declare function createMigration(migrationsDir: string, name: string, autoContent?: {
|
|
81
102
|
up: string;
|
|
82
103
|
down: string;
|
|
104
|
+
}, options?: {
|
|
105
|
+
recipe?: string;
|
|
83
106
|
}): MigrationFile;
|
|
84
107
|
/**
|
|
85
108
|
* Derive a Postgres advisory lock ID (positive int4) from the database name.
|
package/dist/cli/migrate.js
CHANGED
|
@@ -149,14 +149,70 @@ function checksum(content) {
|
|
|
149
149
|
function isLegacyChecksum(hash) {
|
|
150
150
|
return hash.length < 64;
|
|
151
151
|
}
|
|
152
|
+
/** The sanctioned two-phase (add nullable, batched backfill, swap) recipe. */
|
|
153
|
+
function buildBackfillRecipe() {
|
|
154
|
+
const up = `-- Two-phase backfill scaffold. Every statement below is COMMENTED OUT: fill in
|
|
155
|
+
-- your table, the new column, the old column, and the transform, then uncomment
|
|
156
|
+
-- the phases you need and review before running \`npx turbine migrate up\`.
|
|
157
|
+
--
|
|
158
|
+
-- Phase 1: add the new column as NULLABLE. This is a fast, non-blocking change
|
|
159
|
+
-- (no table rewrite, no long lock), so it is safe to ship ahead of the backfill.
|
|
160
|
+
-- ALTER TABLE "my_table" ADD COLUMN "new_col" text;
|
|
161
|
+
--
|
|
162
|
+
-- Phase 2: backfill in bounded batches. Repeat this UPDATE until it reports
|
|
163
|
+
-- 0 rows affected. \`turbine migrate\` runs each file exactly once, so for large
|
|
164
|
+
-- tables drive the loop from psql or your app rather than inlining it here.
|
|
165
|
+
-- Tune the LIMIT (batch size) to your row width and lock tolerance.
|
|
166
|
+
-- UPDATE "my_table"
|
|
167
|
+
-- SET "new_col" = transform("old_col")
|
|
168
|
+
-- WHERE "new_col" IS NULL
|
|
169
|
+
-- AND "id" IN (
|
|
170
|
+
-- SELECT "id" FROM "my_table" WHERE "new_col" IS NULL LIMIT 5000
|
|
171
|
+
-- );
|
|
172
|
+
--
|
|
173
|
+
-- Phase 3: once every row is populated, enforce NOT NULL.
|
|
174
|
+
-- Note: SET NOT NULL takes an exclusive lock and scans the table. On huge
|
|
175
|
+
-- tables, first ADD CONSTRAINT ... CHECK ("new_col" IS NOT NULL) NOT VALID,
|
|
176
|
+
-- then VALIDATE CONSTRAINT (PG 12+ uses the validated check to skip the scan).
|
|
177
|
+
-- ALTER TABLE "my_table" ALTER COLUMN "new_col" SET NOT NULL;
|
|
178
|
+
--
|
|
179
|
+
-- Phase 4 (optional atomic swap): retire the old column and rename the new one
|
|
180
|
+
-- into its place, in one transaction so readers never see a missing column.
|
|
181
|
+
-- BEGIN;
|
|
182
|
+
-- ALTER TABLE "my_table" RENAME COLUMN "old_col" TO "old_col_retired";
|
|
183
|
+
-- ALTER TABLE "my_table" RENAME COLUMN "new_col" TO "old_col";
|
|
184
|
+
-- COMMIT;`;
|
|
185
|
+
const down = `-- Reverse the Phase 4 atomic swap (only if you ran it).
|
|
186
|
+
-- BEGIN;
|
|
187
|
+
-- ALTER TABLE "my_table" RENAME COLUMN "old_col" TO "new_col";
|
|
188
|
+
-- ALTER TABLE "my_table" RENAME COLUMN "old_col_retired" TO "old_col";
|
|
189
|
+
-- COMMIT;
|
|
190
|
+
--
|
|
191
|
+
-- If you stopped after phases 1 to 3, drop the added column instead:
|
|
192
|
+
-- ALTER TABLE "my_table" DROP COLUMN "new_col";`;
|
|
193
|
+
return { up, down };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Registry of migration recipes, keyed by `--recipe <name>`. New recipes slot
|
|
197
|
+
* in here without touching {@link createMigration} or the CLI handler.
|
|
198
|
+
*/
|
|
199
|
+
export const MIGRATION_RECIPES = {
|
|
200
|
+
backfill: {
|
|
201
|
+
description: 'Two-phase column backfill (add nullable, batched UPDATE, SET NOT NULL, rename swap)',
|
|
202
|
+
build: buildBackfillRecipe,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
152
205
|
// ---------------------------------------------------------------------------
|
|
153
206
|
// Commands
|
|
154
207
|
// ---------------------------------------------------------------------------
|
|
155
208
|
/**
|
|
156
209
|
* Create a new migration file.
|
|
157
|
-
*
|
|
210
|
+
*
|
|
211
|
+
* - `autoContent`: pre-populate UP/DOWN from a schema diff.
|
|
212
|
+
* - `options.recipe`: scaffold a named recipe (see {@link MIGRATION_RECIPES}).
|
|
213
|
+
* Mutually exclusive with `autoContent`; an unknown recipe throws.
|
|
158
214
|
*/
|
|
159
|
-
export function createMigration(migrationsDir, name, autoContent) {
|
|
215
|
+
export function createMigration(migrationsDir, name, autoContent, options) {
|
|
160
216
|
mkdirSync(migrationsDir, { recursive: true });
|
|
161
217
|
const now = new Date();
|
|
162
218
|
const ts = formatTimestamp(now);
|
|
@@ -164,7 +220,25 @@ export function createMigration(migrationsDir, name, autoContent) {
|
|
|
164
220
|
const filename = `${ts}_${safeName}.sql`;
|
|
165
221
|
const filePath = join(migrationsDir, filename);
|
|
166
222
|
let template;
|
|
167
|
-
if (
|
|
223
|
+
if (options?.recipe) {
|
|
224
|
+
const recipe = MIGRATION_RECIPES[options.recipe];
|
|
225
|
+
if (!recipe) {
|
|
226
|
+
const known = Object.keys(MIGRATION_RECIPES).join(', ') || '(none)';
|
|
227
|
+
throw new MigrationError(`[turbine] Unknown migration recipe "${options.recipe}". Available recipes: ${known}`);
|
|
228
|
+
}
|
|
229
|
+
const body = recipe.build(name);
|
|
230
|
+
template = `-- Migration: ${name} (${options.recipe} recipe scaffold)
|
|
231
|
+
-- Created: ${now.toISOString()}
|
|
232
|
+
-- Fill in the placeholders and review before running: npx turbine migrate up
|
|
233
|
+
|
|
234
|
+
-- UP
|
|
235
|
+
${body.up}
|
|
236
|
+
|
|
237
|
+
-- DOWN
|
|
238
|
+
${body.down}
|
|
239
|
+
`;
|
|
240
|
+
}
|
|
241
|
+
else if (autoContent) {
|
|
168
242
|
template = `-- Migration: ${name} (auto-generated from schema diff)
|
|
169
243
|
-- Created: ${now.toISOString()}
|
|
170
244
|
-- Review this file before running: npx turbine migrate up
|