feat(audit-wave-1): real db:migrate runner with CONCURRENTLY support
Closes Wave 1.1 (CRITICAL): the production-grade migration runner the
audit flagged as missing.
Why drizzle-kit migrate alone wasn't enough:
- Wraps every migration in a single transaction. Postgres forbids
CREATE INDEX CONCURRENTLY inside a transaction (25001), so the
6 composite indexes in 0052_audit_critical_fixes.sql never landed
in prod.
- db:push silently diverges from migration-tracked truth on DDL the
kit can't infer from the schema (CHECK constraints, partial unique
indexes, the berth-pdf circular FK).
scripts/db-migrate.ts:
- Reads journal-ordered migrations from src/lib/db/migrations.
- Tracks applied state in drizzle.__drizzle_migrations (same schema
Drizzle's own tools use).
- Splits each migration on `--> statement-breakpoint`.
- Classifies each statement: CREATE/REINDEX/DROP INDEX CONCURRENTLY
→ outside transaction; everything else → batched in one tx per
migration. Transactional batch runs first, CONCURRENTLY second.
Three modes:
- `pnpm db:migrate` — apply pending migrations
- `pnpm db:migrate:status` — diff applied vs disk
- `pnpm db:migrate:baseline` — mark all as applied without running
them. Use ONCE per env when schema
was bootstrapped via db:push.
Also fixes scripts/tsc-staged.mjs: temp tsconfig now lives in
`node_modules/.cache/tsc-staged/` (was /tmp) AND explicitly lists
`types: [node, react, react-dom]` so @types/* auto-resolution works
when `include: []` short-circuits TS's default discovery.
For the existing prod cutover:
After `db:migrate:baseline`, manually verify 0052's composite
indexes exist:
SELECT indexname FROM pg_indexes
WHERE indexname IN ('idx_files_port_client', 'idx_files_port_company',
'idx_files_port_yacht', 'idx_docs_port_client',
'idx_docs_port_company', 'idx_docs_port_yacht');
If missing, paste 0052's CREATE INDEX CONCURRENTLY statements into
a `psql` session directly (each runs OUTSIDE a transaction).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,7 @@
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { join, relative, resolve } from 'node:path';
|
||||
|
||||
const cwd = process.cwd();
|
||||
@@ -26,7 +25,13 @@ if (files.length === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), 'tsc-staged-'));
|
||||
// Temp tsconfig lives inside the project tree (not /tmp) so @types/*
|
||||
// resolution walks up to node_modules. tsc's "atTypes" auto-discovery
|
||||
// is anchored to the tsconfig's directory, so a temp config in /tmp
|
||||
// would miss our @types/node, @types/react, etc.
|
||||
const baseDir = join(cwd, 'node_modules/.cache/tsc-staged');
|
||||
mkdirSync(baseDir, { recursive: true });
|
||||
const tmpDir = mkdtempSync(join(baseDir, 'run-'));
|
||||
const tmpConfig = join(tmpDir, 'tsconfig.json');
|
||||
|
||||
const relFiles = files.map((f) => relative(tmpDir, resolve(cwd, f)));
|
||||
@@ -36,7 +41,14 @@ writeFileSync(
|
||||
JSON.stringify(
|
||||
{
|
||||
extends: relative(tmpDir, join(cwd, 'tsconfig.json')),
|
||||
compilerOptions: { noEmit: true, skipLibCheck: true },
|
||||
compilerOptions: {
|
||||
noEmit: true,
|
||||
skipLibCheck: true,
|
||||
// Explicitly list `types` so the @types/* auto-discovery
|
||||
// finds them — without this, the temp-tsconfig location
|
||||
// anchors discovery to .cache/ and misses node/react/etc.
|
||||
types: ['node', 'react', 'react-dom'],
|
||||
},
|
||||
files: relFiles,
|
||||
include: [],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user