123 lines
3.9 KiB
JavaScript
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,
|
|
});
|
|
}
|
|
|