241 lines
10 KiB
JavaScript
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;
|
|
});
|
|
});
|