Files
SnowLuma/.pnpm-store/v11/files/a0/9d38ae3146a766d605c9414030bca8b68e84881703106dafcb56151c71f6506d244221c1216b28818cb9ab8b1edbe742d22bebf627c2326bd2864e43499e1e
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Canceled after 0s
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

158 lines
6.1 KiB
Plaintext

// `sqlite3.migrate(db, migrations)` (Phase 5): a dependency-free,
// `PRAGMA user_version`-based sequential migration runner. No driver
// ships one (libsql ships only a CLI); keeping it opt-in and tiny is the
// point.
//
// Migrations are either an array of { name, sql } / { name, up(db) }
// entries, or a directory path of `NNN-name.sql` files (sorted by the
// numeric prefix, falling back to lexicographic). Each pending migration
// runs inside one transaction (a JS `up` body can await; a raw `sql`
// script may contain several statements) and bumps `user_version` inside
// that transaction, so an interrupted run leaves nothing half-applied.
import { readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
/**
* One migration.
*
* @typedef {object} Migration
* @property {string} name the migration's name (for errors and reporting).
* @property {string} [sql] the SQL script (or use `up`).
* @property {string} [up_sql] the normalized SQL script (internal).
* @property {(tx: import('./native.js').Database) => unknown | Promise<unknown>} [up]
* a programmatic body; receives the transaction connection.
*/
/**
* Normalizes the migrations argument into an ordered name/body list.
*
* @param {string | Migration[]} migrations a directory path or a list.
* @returns {Migration[]} the ordered migrations.
* @throws {TypeError} for a malformed list or unreadable directory.
* @private
*/
function loadMigrations(migrations) {
if (typeof migrations === 'string') {
let entries;
try {
entries = readdirSync(migrations);
} catch (err) {
throw new TypeError(
`migrate() cannot read the migrations directory ${migrations}: ` +
/** @type {Error} */ (err).message,
);
}
return entries
.filter((entry) => entry.endsWith('.sql'))
.map((entry) => {
const stem = entry.slice(0, -4);
return /** @type {Migration} */ ({
name: stem,
up_sql: readFileSync(path.join(migrations, entry), 'utf8'),
});
})
.sort((a, b) => {
const na = Number.parseInt(a.name ?? '', 10);
const nb = Number.parseInt(b.name ?? '', 10);
if (Number.isInteger(na) && Number.isInteger(nb) && na !== nb) {
return na - nb;
}
return (a.name ?? '').localeCompare(b.name ?? '');
});
}
if (!Array.isArray(migrations)) {
throw new TypeError(
'migrate() requires an array of migrations or a directory path',
);
}
return migrations.map((entry, i) => {
if (
entry === null ||
typeof entry !== 'object' ||
typeof entry.name !== 'string' ||
(typeof entry.up !== 'function' &&
typeof entry.up_sql !== 'string' &&
typeof entry.sql !== 'string')
) {
throw new TypeError(
`migrate()[${i}] must be { name, sql } or { name, up }`,
);
}
if (entry.up !== undefined && (entry.sql || entry.up_sql)) {
throw new TypeError(
`migrate()[${i}] (${entry.name}) has both sql and up; pick one`,
);
}
return /** @type {Migration} */ ({
name: entry.name,
up: entry.up,
up_sql: entry.up_sql ?? entry.sql,
});
});
}
/**
* Runs every pending migration against the connection, in order, each in
* one transaction with `PRAGMA user_version` tracking the position.
* Idempotent: already-applied migrations (by position — the first
* `user_version` entries) are skipped.
*
* @param {import('./native.js').Database} db the connection.
* @param {string | Migration[]} migrations a directory of `NNN-name.sql`
* files or a list of `{ name, sql }` / `{ name, up }` entries.
* @returns {Promise<{ applied: string[], from: number, to: number }>} the
* applied names and the version range.
* @throws {TypeError} when the migrations are malformed; rejects with the
* failing migration's error (its work rolled back).
* @since 9.1.0
* @example
* await sqlite3.migrate(db, [
* { name: '0001-create-users', sql: 'CREATE TABLE users (id INTEGER PRIMARY KEY)' },
* { name: '0002-seed', up: (tx) => tx.run('INSERT INTO users DEFAULT VALUES') },
* ]);
*/
export async function migrate(db, migrations) {
const list = loadMigrations(migrations);
const from =
/** @type {number} */ (
await db.pragma('user_version', { simple: true })
) ?? 0;
/** @type {string[]} */
const applied = [];
for (let i = from; i < list.length; i++) {
const migration = list[i];
try {
await db.transaction(
async (tx) => {
if (migration.up !== undefined) {
await migration.up(tx);
} else {
await tx.exec(/** @type {string} */ (migration.up_sql));
}
await tx.run(`PRAGMA user_version = ${i + 1}`);
},
{ mode: 'immediate' },
);
applied.push(migration.name);
} catch (err) {
const context = `migration ${migration.name} (version ${
i + 1
}) failed`;
// Prefix the failure in place when it is an Error (keeping its
// code/errno/stack), and wrap anything else — a migration that
// rejects with a string must not make this throw a TypeError
// about assigning to `message`.
if (err instanceof Error) {
err.message = `${context}: ${err.message}`;
throw err;
}
throw new Error(`${context}: ${String(err)}`, { cause: err });
}
}
// `to` is where the database now is, which is `from` when nothing was
// pending — including a database ahead of the list (a rollback of the
// application without a rollback of its schema).
return { applied, from, to: Math.max(from, list.length) };
}