| import { isWindows, skipIfSQLiteMissing } from '../common/index.mjs'; |
| import tmpdir from '../common/tmpdir.js'; |
| import { join } from 'node:path'; |
| import { describe, test } from 'node:test'; |
| import { writeFileSync } from 'node:fs'; |
| import { pathToFileURL } from 'node:url'; |
| skipIfSQLiteMissing(); |
| const { backup, DatabaseSync } = await import('node:sqlite'); |
| |
| const isRoot = !isWindows && process.getuid() === 0; |
| |
| 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', { skip: isRoot }, 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' |
| }); |
| }); |
| |
| test('backup has correct name and length', (t) => { |
| t.assert.strictEqual(backup.name, 'backup'); |
| t.assert.strictEqual(backup.length, 2); |
| }); |