Files
Learn_System/backend/src/db/migrations-runner.js
T
Maxim Dolgolyov 41d4465905 feat: versioned migrations runner (phase 1 — no behaviour change)
Adds backend/src/db/migrations-runner.js:
- Tracks applied migrations in _migrations table
- Applies .sql files from src/db/migrations/ in alphabetical order
- Each file runs in a transaction — fail-fast, no partial state
- `migrate:bootstrap` marks 000_baseline.sql as applied on existing DBs

000_baseline.sql — full schema snapshot from prod DB (168 objects, 2026-05-06).
Removed stale PostgreSQL migration files (001_init.sql, 002_constraints.sql)
that used SERIAL/EXTENSION syntax incompatible with SQLite.

npm scripts:
  migrate           → migrations-runner.js (versioned)
  migrate:bootstrap → mark baseline applied (run once per env)
  migrate:legacy    → legacy migrate.js (kept for reference)

On prod DB after `migrate:bootstrap`: "Nothing to apply — schema is up to date".
Legacy migrate.js still in place; tests still use it via setup.js (unchanged).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 17:47:59 +03:00

84 lines
2.4 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const db = require('./db');
const MIGRATIONS_DIR = path.join(__dirname, 'migrations');
function init() {
db.exec(`
CREATE TABLE IF NOT EXISTS _migrations (
filename TEXT PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
}
function listFiles() {
if (!fs.existsSync(MIGRATIONS_DIR)) return [];
return fs.readdirSync(MIGRATIONS_DIR)
.filter(f => f.endsWith('.sql'))
.sort();
}
function applied() {
return new Set(db.prepare('SELECT filename FROM _migrations').all().map(r => r.filename));
}
function run() {
init();
const done = applied();
const files = listFiles();
const pending = files.filter(f => !done.has(f));
if (pending.length === 0) {
console.log('[migrate] Nothing to apply — schema is up to date');
return;
}
for (const file of pending) {
const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, file), 'utf8');
console.log(`[migrate] Applying ${file}...`);
try {
db.exec('BEGIN');
db.exec(sql);
db.prepare('INSERT INTO _migrations (filename) VALUES (?)').run(file);
db.exec('COMMIT');
console.log(`[migrate] OK: ${file}`);
} catch (e) {
db.exec('ROLLBACK');
console.error(`[migrate] FAILED at ${file}: ${e.message}`);
process.exit(1);
}
}
console.log(`[migrate] Applied ${pending.length} migration(s)`);
}
/* Mark 000_baseline.sql as applied on existing DBs without running it.
Run once per environment after first deploy of this system. */
function markBaseline() {
init();
const hasUsers = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='users'").get();
if (hasUsers) {
const done = applied();
if (!done.has('000_baseline.sql')) {
db.prepare('INSERT INTO _migrations (filename) VALUES (?)').run('000_baseline.sql');
console.log('[migrate] Marked 000_baseline.sql as applied (existing DB — not re-run)');
} else {
console.log('[migrate] 000_baseline.sql already marked as applied');
}
} else {
console.log('[migrate] No users table found — run `npm run migrate` first to initialize schema');
process.exit(1);
}
}
if (require.main === module) {
const cmd = process.argv[2];
if (cmd === 'bootstrap') markBaseline();
else run();
}
module.exports = { run, markBaseline };