u
This commit is contained in:
121
src/providers/sqlite3/migrate.js
Normal file
121
src/providers/sqlite3/migrate.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* SQLite schema versioning (see ../schemaMigrations.js).
|
||||
*
|
||||
* Step context: `{ db, exec, getVersion, assertVersion, setVersion }` where
|
||||
* `db` is the raw better-sqlite3 Database and `exec(sql)` runs DDL/DML.
|
||||
* Each step runs in `BEGIN IMMEDIATE … COMMIT` with foreign key enforcement
|
||||
* switched off (SQLite's recommended procedure for table rebuilds);
|
||||
* `PRAGMA foreign_key_check` must pass before commit.
|
||||
*/
|
||||
|
||||
import {
|
||||
BASE_SCHEMA_VERSION,
|
||||
assertStepResult,
|
||||
latestSchemaVersion,
|
||||
makeVersionContext,
|
||||
migrateSchema,
|
||||
parseSchemaVersion,
|
||||
} from '../schemaMigrations.js';
|
||||
|
||||
export const UPGRADE_DIR = new URL('./upgrade/', import.meta.url);
|
||||
export const DOWNGRADE_DIR = new URL('./downgrade/', import.meta.url);
|
||||
|
||||
/** Schema version a fresh database is created at. */
|
||||
export const SCHEMA_VERSION = latestSchemaVersion(UPGRADE_DIR);
|
||||
|
||||
function hasTable(db, name) {
|
||||
return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name);
|
||||
}
|
||||
|
||||
function hasAnyTable(db) {
|
||||
return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads `meta.version`, or `null` when the meta table/row is missing.
|
||||
* @param {import('better-sqlite3').Database} db
|
||||
* @returns {number|null}
|
||||
*/
|
||||
export function readSchemaVersion(db) {
|
||||
if (!hasTable(db, 'meta')) return null;
|
||||
const row = db.prepare(`SELECT value FROM meta WHERE "key" = 'version'`).get();
|
||||
return row ? parseSchemaVersion(row.value) : null;
|
||||
}
|
||||
|
||||
function writeSchemaVersion(db, version) {
|
||||
db.prepare(`INSERT INTO meta ("key", value) VALUES ('version', ?)
|
||||
ON CONFLICT("key") DO UPDATE SET value = excluded.value`).run(String(version));
|
||||
}
|
||||
|
||||
function makeRunStep(db) {
|
||||
return async (step, fn) => {
|
||||
const fkWasOn = db.pragma('foreign_keys', { simple: true }) === 1;
|
||||
if (fkWasOn) db.pragma('foreign_keys = OFF');
|
||||
db.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
const ctx = {
|
||||
db,
|
||||
exec: (sql) => db.exec(sql),
|
||||
...makeVersionContext(step.file, () => readSchemaVersion(db), (v) => writeSchemaVersion(db, v)),
|
||||
};
|
||||
await fn(ctx);
|
||||
assertStepResult(step, readSchemaVersion(db));
|
||||
const violations = db.pragma('foreign_key_check');
|
||||
if (violations.length > 0) {
|
||||
throw new Error(`Schema step ${step.file} left ${violations.length} foreign key violation(s)`);
|
||||
}
|
||||
db.exec('COMMIT');
|
||||
} catch (err) {
|
||||
if (db.inTransaction) db.exec('ROLLBACK');
|
||||
throw err;
|
||||
} finally {
|
||||
if (fkWasOn) db.pragma('foreign_keys = ON');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrades an existing database to `SCHEMA_VERSION`. Must run BEFORE the
|
||||
* `schema.js` scripts so they never meet an outdated table shape.
|
||||
* Fresh (empty) databases are skipped — `INIT_META` stamps them.
|
||||
* A non-empty database without a version row is treated as version 1.
|
||||
* @param {import('better-sqlite3').Database} db
|
||||
* @param {(message: string) => void} [log]
|
||||
*/
|
||||
export async function upgradeSchema(db, log) {
|
||||
if (!hasAnyTable(db)) return 0;
|
||||
let current = readSchemaVersion(db);
|
||||
if (current === null) {
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS meta ("key" TEXT PRIMARY KEY, value TEXT NOT NULL)`);
|
||||
writeSchemaVersion(db, BASE_SCHEMA_VERSION);
|
||||
current = BASE_SCHEMA_VERSION;
|
||||
}
|
||||
return migrateSchema({
|
||||
direction: 'upgrade',
|
||||
dirUrl: UPGRADE_DIR,
|
||||
current,
|
||||
target: SCHEMA_VERSION,
|
||||
runStep: makeRunStep(db),
|
||||
log,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Downgrades the database to `target` (CLI only, never at server start).
|
||||
* @param {import('better-sqlite3').Database} db
|
||||
* @param {number} target
|
||||
* @param {(message: string) => void} [log]
|
||||
*/
|
||||
export async function downgradeSchema(db, target, log) {
|
||||
const current = readSchemaVersion(db);
|
||||
if (current === null) throw new Error('Database has no schema version (meta table missing)');
|
||||
return migrateSchema({
|
||||
direction: 'downgrade',
|
||||
dirUrl: DOWNGRADE_DIR,
|
||||
current,
|
||||
target,
|
||||
runStep: makeRunStep(db),
|
||||
log,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user