import '../common/index.mjs'; import tmpdir from '../common/tmpdir.js'; import { join } from 'node:path'; import { backup, DatabaseSync } from 'node:sqlite'; import { describe, test } from 'node:test'; import { writeFileSync } from 'node:fs'; import { pathToFileURL } from 'node:url'; let cnt = 0; tmpdir.refresh(); function nextDb() { return join(tmpdir.path, `database-${cnt++}.db`); } function makeSourceDb(dbPath = ':memory:') { const database = new DatabaseSync(dbPath); database.exec(` CREATE TABLE data( key INTEGER PRIMARY KEY, value TEXT ) STRICT `); const insert = database.prepare('INSERT INTO data (key, value) VALUES (?, ?)'); for (let i = 1; i <= 2; i++) { insert.run(i, `value-${i}`); } return database; } describe('backup()', () => { test('throws if the source database is not provided', (t) => { t.assert.throws(() => { backup(); }, { code: 'ERR_INVALID_ARG_TYPE', message: 'The "sourceDb" argument must be an object.' }); }); test('throws if path is not a string, URL, or Buffer', (t) => { const database = makeSourceDb(); t.assert.throws(() => { backup(database); }, { code: 'ERR_INVALID_ARG_TYPE', message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.' }); t.assert.throws(() => { backup(database, {}); }, { code: 'ERR_INVALID_ARG_TYPE', message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.' }); }); test('throws if the database path contains null bytes', (t) => { const database = makeSourceDb(); t.assert.throws(() => { backup(database, Buffer.from('l\0cation')); }, { code: 'ERR_INVALID_ARG_TYPE', message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.' }); t.assert.throws(() => { backup(database, 'l\0cation'); }, { code: 'ERR_INVALID_ARG_TYPE', message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.' }); }); test('throws if options is not an object', (t) => { const database = makeSourceDb(); t.assert.throws(() => { backup(database, 'hello.db', 'invalid'); }, { code: 'ERR_INVALID_ARG_TYPE', message: 'The "options" argument must be an object.' }); }); test('throws if any of provided options is invalid', (t) => { const database = makeSourceDb(); t.assert.throws(() => { backup(database, 'hello.db', { source: 42 }); }, { code: 'ERR_INVALID_ARG_TYPE', message: 'The "options.source" argument must be a string.' }); t.assert.throws(() => { backup(database, 'hello.db', { target: 42 }); }, { code: 'ERR_INVALID_ARG_TYPE', message: 'The "options.target" argument must be a string.' }); t.assert.throws(() => { backup(database, 'hello.db', { rate: 'invalid' }); }, { code: 'ERR_INVALID_ARG_TYPE', message: 'The "options.rate" argument must be an integer.' }); t.assert.throws(() => { backup(database, 'hello.db', { progress: 'invalid' }); }, { code: 'ERR_INVALID_ARG_TYPE', message: 'The "options.progress" argument must be a function.' }); }); }); test('database backup', async (t) => { const progressFn = t.mock.fn(); const database = makeSourceDb(); const destDb = nextDb(); await backup(database, destDb, { rate: 1, progress: progressFn, }); const backupDb = new DatabaseSync(destDb); const rows = backupDb.prepare('SELECT * FROM data').all(); // The source database has two pages - using the default page size -, // so the progress function should be called once (the last call is not made since // the promise resolves) t.assert.strictEqual(progressFn.mock.calls.length, 1); t.assert.deepStrictEqual(progressFn.mock.calls[0].arguments, [{ totalPages: 2, remainingPages: 1 }]); t.assert.deepStrictEqual(rows, [ { __proto__: null, key: 1, value: 'value-1' }, { __proto__: null, key: 2, value: 'value-2' }, ]); t.after(() => { database.close(); backupDb.close(); }); }); test('backup database using location as URL', async (t) => { const database = makeSourceDb(); const destDb = pathToFileURL(nextDb()); t.after(() => { database.close(); }); await backup(database, destDb); const backupDb = new DatabaseSync(destDb); t.after(() => { backupDb.close(); }); const rows = backupDb.prepare('SELECT * FROM data').all(); t.assert.deepStrictEqual(rows, [ { __proto__: null, key: 1, value: 'value-1' }, { __proto__: null, key: 2, value: 'value-2' }, ]); }); test('backup database using location as Buffer', async (t) => { const database = makeSourceDb(); const destDb = Buffer.from(nextDb()); t.after(() => { database.close(); }); await backup(database, destDb); const backupDb = new DatabaseSync(destDb); t.after(() => { backupDb.close(); }); const rows = backupDb.prepare('SELECT * FROM data').all(); t.assert.deepStrictEqual(rows, [ { __proto__: null, key: 1, value: 'value-1' }, { __proto__: null, key: 2, value: 'value-2' }, ]); }); test('database backup in a single call', async (t) => { const progressFn = t.mock.fn(); const database = makeSourceDb(); const destDb = nextDb(); // Let rate to be default (100) to backup in a single call await backup(database, destDb, { progress: progressFn, }); const backupDb = new DatabaseSync(destDb); const rows = backupDb.prepare('SELECT * FROM data').all(); t.assert.strictEqual(progressFn.mock.calls.length, 0); t.assert.deepStrictEqual(rows, [ { __proto__: null, key: 1, value: 'value-1' }, { __proto__: null, key: 2, value: 'value-2' }, ]); t.after(() => { database.close(); backupDb.close(); }); }); test('throws exception when trying to start backup from a closed database', (t) => { t.assert.throws(() => { const database = new DatabaseSync(':memory:'); database.close(); backup(database, 'backup.db'); }, { code: 'ERR_INVALID_STATE', message: 'database is not open' }); }); test('throws if URL is not file: scheme', (t) => { const database = new DatabaseSync(':memory:'); t.after(() => { database.close(); }); t.assert.throws(() => { backup(database, new URL('http://example.com/backup.db')); }, { code: 'ERR_INVALID_URL_SCHEME', message: 'The URL must be of scheme file:', }); }); test('database backup fails when dest file is not writable', async (t) => { const readonlyDestDb = nextDb(); writeFileSync(readonlyDestDb, '', { mode: 0o444 }); const database = makeSourceDb(); await t.assert.rejects(async () => { await backup(database, readonlyDestDb); }, { code: 'ERR_SQLITE_ERROR', message: 'attempt to write a readonly database' }); }); test('backup fails when progress function throws', async (t) => { const database = makeSourceDb(); const destDb = nextDb(); const progressFn = t.mock.fn(() => { throw new Error('progress error'); }); await t.assert.rejects(async () => { await backup(database, destDb, { rate: 1, progress: progressFn, }); }, { message: 'progress error' }); }); test('backup fails when source db is invalid', async (t) => { const database = makeSourceDb(); const destDb = nextDb(); await t.assert.rejects(async () => { await backup(database, destDb, { rate: 1, source: 'invalid', }); }, { message: 'unknown database invalid' }); }); test('backup fails when path cannot be opened', async (t) => { const database = makeSourceDb(); await t.assert.rejects(async () => { await backup(database, `${tmpdir.path}/invalid/backup.db`); }, { message: 'unable to open database file' }); });