better-sqlite3/test/36.database.backup.js
2021-05-11 01:05:54 -05:00

241 lines
10 KiB
JavaScript

'use strict';
const { existsSync, writeFileSync, readFileSync } = require('fs');
const Database = require('../.');
describe('Database#backup()', function () {
beforeEach(function () {
this.db = new Database(util.next());
this.db.prepare("CREATE TABLE entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)").run();
this.db.prepare("INSERT INTO entries WITH RECURSIVE temp(a, b, c, d, e) AS (SELECT 'foo', 1, 3.14, x'dddddddd', NULL UNION ALL SELECT a, b + 1, c, d, e FROM temp LIMIT 5) SELECT * FROM temp").run();
});
afterEach(function () {
this.db.close();
});
const fulfillsWith = (value, p) => p.then(v => void expect(v).to.deep.equal(value));
const rejectsWith = (type, p) => {
const shouldReject = () => { throw new Error('Promise should have been rejected') };
const reasonIs = (reason) => { if (!(reason instanceof type)) throw reason; }
return p.then(shouldReject, reasonIs);
};
it('should be rejected when destination is not a string', async function () {
await rejectsWith(TypeError, this.db.backup());
await rejectsWith(TypeError, this.db.backup(null));
await rejectsWith(TypeError, this.db.backup(0));
await rejectsWith(TypeError, this.db.backup(123));
await rejectsWith(TypeError, this.db.backup(new String(util.next())));
await rejectsWith(TypeError, this.db.backup(() => util.next()));
await rejectsWith(TypeError, this.db.backup([util.next()]));
});
it('should not allow an empty destination string', async function () {
await rejectsWith(TypeError, this.db.backup(''));
await rejectsWith(TypeError, this.db.backup(' \t\n '));
});
it('should not allow a :memory: destination', async function () {
await rejectsWith(TypeError, this.db.backup(':memory:'));
expect(existsSync(':memory:')).to.be.false;
});
it('should backup the database and fulfill the returned promise', async function () {
expect(existsSync(this.db.name)).to.be.true;
expect(existsSync(util.next())).to.be.false;
const promise = this.db.backup(util.current());
expect(existsSync(util.current())).to.be.false;
await fulfillsWith({ totalPages: 2, remainingPages: 0 }, promise);
expect(existsSync(this.db.name)).to.be.true;
expect(existsSync(util.current())).to.be.true;
const rows = this.db.prepare('SELECT * FROM entries').all();
this.db.close();
this.db = new Database(util.current());
expect(this.db.prepare('SELECT * FROM entries').all()).to.deep.equal(rows);
});
it('should be rejected if the directory does not exist', async function () {
expect(existsSync(util.next())).to.be.false;
const filepath = `temp/nonexistent/abcfoobar123/${util.current()}`;
await rejectsWith(TypeError, this.db.backup(filepath));
expect(existsSync(filepath)).to.be.false;
expect(existsSync(util.current())).to.be.false;
});
it('should be rejected if a database cannot be opened at the destination', async function () {
writeFileSync(util.next(), 'not a database file');
await rejectsWith(Database.SqliteError, this.db.backup(util.current()));
expect(readFileSync(util.current(), 'utf8')).to.equal('not a database file');
});
it('should accept the "attached" option', async function () {
const source = this.db.name;
const destination = util.next();
let promise;
this.db.close();
this.db = new Database(':memory:');
this.db.prepare('ATTACH ? AS cool_db').run(source);
expect(existsSync(source)).to.be.true;
expect(existsSync(destination)).to.be.false;
await fulfillsWith({ totalPages: 2, remainingPages: 0 },
this.db.backup(destination, { attached: 'cool_db' }));
expect(existsSync(source)).to.be.true;
expect(existsSync(destination)).to.be.true;
const rows = this.db.prepare('SELECT * FROM cool_db.entries').all();
this.db.close();
this.db = new Database(destination);
expect(this.db.prepare('SELECT * FROM main.entries').all()).to.deep.equal(rows);
});
it('should accept the "progress" option', async function () {
expect(existsSync(this.db.name)).to.be.true;
expect(existsSync(util.next())).to.be.false;
const calls = [];
const promise = this.db.backup(util.current(), { progress(...args) {
calls.push([this, ...args]);
} });
expect(existsSync(util.current())).to.be.false;
await fulfillsWith({ totalPages: 2, remainingPages: 0 }, promise);
expect(existsSync(this.db.name)).to.be.true;
expect(existsSync(util.current())).to.be.true;
const rows = this.db.prepare('SELECT * FROM entries').all();
this.db.close();
this.db = new Database(util.current());
expect(this.db.prepare('SELECT * FROM entries').all()).to.deep.equal(rows);
expect(calls).to.deep.equal([[undefined, { totalPages: 2, remainingPages: 2 }]]);
});
it('should allow control over transfer sizes via the progress callback', async function () {
let transferSize = 0;
const expected = [];
const actual = [];
const promise = this.db.backup(util.next(), { progress(state) {
actual.push(state);
return transferSize;
} });
promise.catch(() => {});
expect(actual).to.deep.equal(expected);
while (!actual.length) await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 2 });
expect(actual).to.deep.equal(expected);
await new Promise(setImmediate);
transferSize = 1;
await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 2 });
expected.push({ totalPages: 2, remainingPages: 2 });
expect(actual).to.deep.equal(expected);
await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 1 });
expect(actual).to.deep.equal(expected);
const payload = Buffer.alloc(4096 * 5).fill(0x7a).toString();
this.db.prepare('INSERT INTO entries (a, b) VALUES (?, 999)').run(payload);
transferSize = Infinity;
await new Promise(setImmediate);
expected.push({ totalPages: 7, remainingPages: 5 });
expect(actual).to.deep.equal(expected);
await new Promise(setImmediate);
expect(actual).to.deep.equal(expected);
await fulfillsWith({ totalPages: 7, remainingPages: 0 }, promise);
this.db.close();
this.db = new Database(util.current());
expect(this.db.prepare('SELECT a FROM entries WHERE b = 999').pluck().get()).to.deep.equal(payload);
});
it('should be aborted if an error is thrown inside the progress callback', async function () {
const promise = this.db.backup(util.next(), { progress: () => { throw new SyntaxError('foo'); } });
await rejectsWith(SyntaxError, promise);
});
it('should be aborted if the progress callback returns a non-number', async function () {
const backup = x => this.db.backup(util.next(), { progress: () => x });
await rejectsWith(TypeError, backup(null));
await rejectsWith(TypeError, backup(new Number(1)));
await rejectsWith(TypeError, backup(() => 1));
await rejectsWith(TypeError, backup([1]));
await rejectsWith(TypeError, backup('1'));
});
it('should rollback an aborted backup file if it was not newly created', async function () {
const otherDb = new Database(util.next());
try {
otherDb.prepare('CREATE TABLE foo (bar)').run()
otherDb.prepare('INSERT INTO foo VALUES (2), (8)').run();
} finally {
otherDb.close();
}
let error;
let transferSize = 0;
const expected = [];
const actual = [];
const promise = this.db.backup(util.current(), { progress(state) {
actual.push(state);
if (error) throw error;
return transferSize;
} });
promise.catch(() => {});
expect(actual).to.deep.equal(expected);
while (!actual.length) await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 2 });
expect(actual).to.deep.equal(expected);
transferSize = 1;
await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 2 });
expect(actual).to.deep.equal(expected);
transferSize = 0;
await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 1 });
expect(actual).to.deep.equal(expected);
error = new SyntaxError('foo');
await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 1 });
expect(actual).to.deep.equal(expected);
await rejectsWith(SyntaxError, promise);
expect(actual).to.deep.equal(expected);
expect(existsSync(util.current())).to.be.true;
this.db.close();
this.db = new Database(util.current());
expect(this.db.prepare('SELECT bar FROM foo').pluck().all()).to.deep.equal([2, 8]);
});
it('should delete an aborted backup file if it was newly created', async function () {
let error;
let transferSize = 0;
const expected = [];
const actual = [];
const promise = this.db.backup(util.next(), { progress(state) {
actual.push(state);
if (error) throw error;
return transferSize;
} });
promise.catch(() => {});
expect(actual).to.deep.equal(expected);
while (!actual.length) await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 2 });
expect(actual).to.deep.equal(expected);
transferSize = 1;
await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 2 });
expect(actual).to.deep.equal(expected);
transferSize = 0;
await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 1 });
expect(actual).to.deep.equal(expected);
error = new SyntaxError('foo');
await new Promise(setImmediate);
expected.push({ totalPages: 2, remainingPages: 1 });
expect(actual).to.deep.equal(expected);
await rejectsWith(SyntaxError, promise);
expect(actual).to.deep.equal(expected);
expect(existsSync(util.current())).to.be.false;
});
it('should be aborted if the connection is closed during a backup', async function () {
let transferSize = 0;
const calls = [];
const promise = this.db.backup(util.next(), { progress(state) {
calls.push(state);
return transferSize;
} });
promise.catch(() => {});
while (!calls.length) await new Promise(setImmediate);
transferSize = 1;
await new Promise(setImmediate);
await new Promise(setImmediate);
this.db.close();
expect(this.db.open).to.be.false;
await rejectsWith(TypeError, promise);
expect(calls).to.deep.equal([
{ totalPages: 2, remainingPages: 2 },
{ totalPages: 2, remainingPages: 2 },
{ totalPages: 2, remainingPages: 1 },
]);
expect(existsSync(util.current())).to.be.false;
});
});