taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 72b3202dc370578435392422cf11f2a07ab3e1ba
parent ace3ce35ddf1c8f6cda9dc7cefc96884ccb77753
Author: Florian Dold <dold@taler.net>
Date:   Thu,  3 Sep 2026 15:33:41 +0200

wallet-core: snapshot the database before rolling back a migration

The restore empties the native tables, which hold everything the
wallet did since migrating.

Diffstat:
Mpackages/taler-wallet-cli/src/index.ts | 9+++++++--
Mpackages/taler-wallet-core/src/db/migration/native.test.ts | 28++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/host-impl.node.ts | 18++++++++++++++----
3 files changed, 49 insertions(+), 6 deletions(-)

diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts @@ -3565,14 +3565,19 @@ advancedCli .requiredArgument("dbfile", clk.STRING, { help: "Wallet database file to roll back.", }) + .requiredOption("backup", ["--backup"], clk.STRING, { + help: "Where to snapshot the database, native records included, before it is rolled back.", + }) .action(async (args) => { // Everything the wallet did since the migration lives in the native // tables and is not carried back, so say so rather than report success // and let it be discovered later. - await rollbackWalletDbMigration(args.dbMigrationRollback.dbfile); + const cmd = args.dbMigrationRollback; + await rollbackWalletDbMigration(cmd.dbfile, cmd.backup); console.log( `rolled back to the database as it was before the migration;` + - ` anything the wallet did since then is not part of it.`, + ` anything the wallet did since then is not part of it` + + ` and is kept only in ${cmd.backup}.`, ); }); diff --git a/packages/taler-wallet-core/src/db/migration/native.test.ts b/packages/taler-wallet-core/src/db/migration/native.test.ts @@ -71,6 +71,7 @@ import { createNativeWalletHost2, inspectWalletDbPath, resolveWalletDbMigration, + rollbackWalletDbMigration, } from "../../host-impl.node.js"; import { acquireSqliteWalletDbOwnership } from "../../host-common.js"; import { WalletApiOperation } from "../../wallet-api-types.js"; @@ -1143,3 +1144,30 @@ test("explicit native migration can be cancelled by progress token", async () => fs.rmSync(dir, { recursive: true, force: true }); } }); + +test("native migration: rollback backs up the native database first", async () => { + const directory = fs.mkdtempSync( + path.join(os.tmpdir(), "wallet-db-rollback-backup-"), + ); + const filename = path.join(directory, "wallet.sqlite3"); + const backupPath = path.join(directory, "native-backup.sqlite3"); + try { + const { db, handle } = await makeMinimalIdbDb(filename); + await migrateWalletDbToNative(db, handle); + await handle.close(); + + await rollbackWalletDbMigration(filename, backupPath); + + assert.strictEqual((await inspectWalletDbPath(filename)).kind, "indexeddb"); + assert.ok(fs.existsSync(backupPath)); + assert.strictEqual((await inspectWalletDbPath(backupPath)).kind, "native"); + + // The backup is never silently overwritten. + await assert.rejects( + () => rollbackWalletDbMigration(filename, backupPath), + /already exists/, + ); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +}); diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts @@ -358,17 +358,27 @@ export async function resolveWalletDbMigration( /** * Undo an in-place migration, from the copy it retained. * - * Everything the wallet did since the migration is in the native tables and - * stays there; what comes back is the database as it was the moment before - * the migration ran. The wallet must not be running. + * What comes back is the database as it was the moment before the migration + * ran. Everything the wallet did since then lives only in the native tables, + * which the restore empties, so the whole file is snapshotted to backupPath + * first. The wallet must not be running. */ -export async function rollbackWalletDbMigration(dbPath: string): Promise<void> { +export async function rollbackWalletDbMigration( + dbPath: string, + backupPath: string, +): Promise<void> { if (!fs.existsSync(dbPath)) { throw Error(`wallet database ${dbPath} does not exist`); } + if (fs.existsSync(backupPath)) { + throw Error(`backup destination ${backupPath} already exists`); + } const imp = await createNodeHelperSqlite3Impl(); const db = await imp.open(dbPath); try { + await ( + await db.prepare("VACUUM INTO $filename") + ).run({ filename: backupPath }); await restoreMigrationBackup(db); } finally { await db.close();