Files
dashlibstorage/src/providers/mssql/migrate.js
2026-10-29 03:17:47 +01:00

123 lines
3.9 KiB
JavaScript

/**
* MSSQL schema versioning (see ../schemaMigrations.js).
*
* Step context: `{ query, getVersion, assertVersion, setVersion }` where
* `query(sql, params)` runs inside the step's transaction (same contract as
* `provider.query`). Note: a column added by `ALTER TABLE` is not visible to
* later statements of the SAME batch — use separate `query()` calls.
*/
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);
/**
* @typedef {(sql: string, params?: object) => Promise<{ rows: any[] }>} QueryFn
*/
/**
* Reads `meta.version`, or `null` when the meta table/row is missing.
* @param {QueryFn} query
* @returns {Promise<number|null>}
*/
export async function readSchemaVersion(query) {
const result = await query(`
IF OBJECT_ID(N'dbo.meta', N'U') IS NOT NULL
SELECT value FROM meta WHERE [key] = N'version'
ELSE
SELECT CAST(NULL AS NVARCHAR(MAX)) AS value WHERE 1 = 0
`);
const row = result.rows[0];
return row ? parseSchemaVersion(row.value) : null;
}
async function writeSchemaVersion(query, version) {
await query(`
UPDATE meta SET value = @version WHERE [key] = N'version';
IF @@ROWCOUNT = 0 INSERT INTO meta ([key], value) VALUES (N'version', @version);
`, { version: String(version) });
}
async function hasAnyTable(query) {
const result = await query(`SELECT TOP 1 1 AS found FROM sys.tables WHERE is_ms_shipped = 0`);
return result.rows.length > 0;
}
/**
* @param {import('./index.js').MssqlProvider} provider
*/
function makeRunStep(provider) {
return async (step, fn) => {
await provider.transaction(async (runner) => {
const query = (sql, params) => runner.query(sql, params);
const ctx = {
query,
...makeVersionContext(step.file, () => readSchemaVersion(query), (v) => writeSchemaVersion(query, v)),
};
await fn(ctx);
assertStepResult(step, await readSchemaVersion(query));
});
};
}
/**
* 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('./index.js').MssqlProvider} provider
* @param {(message: string) => void} [log]
*/
export async function upgradeSchema(provider, log) {
const query = (sql, params) => provider.query(sql, params);
if (!(await hasAnyTable(query))) return 0;
let current = await readSchemaVersion(query);
if (current === null) {
await query(`
IF OBJECT_ID(N'dbo.meta', N'U') IS NULL
CREATE TABLE meta ([key] NVARCHAR(255) NOT NULL, value NVARCHAR(MAX) NOT NULL, CONSTRAINT PK_meta PRIMARY KEY ([key]));
`);
await writeSchemaVersion(query, BASE_SCHEMA_VERSION);
current = BASE_SCHEMA_VERSION;
}
return migrateSchema({
direction: 'upgrade',
dirUrl: UPGRADE_DIR,
current,
target: SCHEMA_VERSION,
runStep: makeRunStep(provider),
log,
});
}
/**
* Downgrades the database to `target` (CLI only, never at server start).
* @param {import('./index.js').MssqlProvider} provider
* @param {number} target
* @param {(message: string) => void} [log]
*/
export async function downgradeSchema(provider, target, log) {
const current = await readSchemaVersion((sql, params) => provider.query(sql, params));
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(provider),
log,
});
}