241 lines
10 KiB
JavaScript
241 lines
10 KiB
JavaScript
'use strict';
|
|
const Database = require('../.');
|
|
|
|
describe('Database#function()', function () {
|
|
beforeEach(function () {
|
|
this.db = new Database(util.next());
|
|
this.get = (SQL, ...args) => this.db.prepare(`SELECT ${SQL}`).pluck().get(args);
|
|
});
|
|
afterEach(function () {
|
|
this.db.close();
|
|
});
|
|
|
|
it('should throw an exception if the correct arguments are not provided', function () {
|
|
expect(() => this.db.function()).to.throw(TypeError);
|
|
expect(() => this.db.function(null)).to.throw(TypeError);
|
|
expect(() => this.db.function('a')).to.throw(TypeError);
|
|
expect(() => this.db.function({})).to.throw(TypeError);
|
|
expect(() => this.db.function(() => {})).to.throw(TypeError);
|
|
expect(() => this.db.function(function b() {})).to.throw(TypeError);
|
|
expect(() => this.db.function({}, function c() {})).to.throw(TypeError);
|
|
expect(() => this.db.function('d', {})).to.throw(TypeError);
|
|
expect(() => this.db.function('e', { fn: function e() {} })).to.throw(TypeError);
|
|
expect(() => this.db.function('f', Object.create(Function.prototype))).to.throw(TypeError);
|
|
expect(() => this.db.function({ name: 'g' }, function g() {})).to.throw(TypeError);
|
|
expect(() => this.db.function(new String('h'), function h() {})).to.throw(TypeError);
|
|
});
|
|
it('should throw an exception if boolean options are provided as non-booleans', function () {
|
|
expect(() => this.db.function('a', { varargs: undefined }, () => {})).to.throw(TypeError);
|
|
expect(() => this.db.function('b', { deterministic: undefined }, () => {})).to.throw(TypeError);
|
|
expect(() => this.db.function('c', { safeIntegers: undefined }, () => {})).to.throw(TypeError);
|
|
});
|
|
it('should throw an exception if the provided name is empty', function () {
|
|
expect(() => this.db.function('', function a() {})).to.throw(TypeError);
|
|
expect(() => this.db.function('', { name: 'b' }, function b() {})).to.throw(TypeError);
|
|
});
|
|
it('should throw an exception if function.length is invalid', function () {
|
|
const length = x => Object.defineProperty(() => {}, 'length', { value: x });
|
|
expect(() => this.db.function('a', length(undefined))).to.throw(TypeError);
|
|
expect(() => this.db.function('b', length(null))).to.throw(TypeError);
|
|
expect(() => this.db.function('c', length('2'))).to.throw(TypeError);
|
|
expect(() => this.db.function('d', length(NaN))).to.throw(TypeError);
|
|
expect(() => this.db.function('e', length(Infinity))).to.throw(TypeError);
|
|
expect(() => this.db.function('f', length(2.000000001))).to.throw(TypeError);
|
|
expect(() => this.db.function('g', length(-0.000000001))).to.throw(TypeError);
|
|
expect(() => this.db.function('h', length(-2))).to.throw(TypeError);
|
|
expect(() => this.db.function('i', length(100.000000001))).to.throw(TypeError);
|
|
expect(() => this.db.function('j', length(101))).to.throw(RangeError);
|
|
});
|
|
it('should register the given function and return the database object', function () {
|
|
expect(this.db.function('a', () => {})).to.equal(this.db);
|
|
expect(this.db.function('b', {}, () => {})).to.equal(this.db);
|
|
expect(this.db.function('c', function x() {})).to.equal(this.db);
|
|
expect(this.db.function('d', {}, function y() {})).to.equal(this.db);
|
|
});
|
|
it('should enable the registered function to be executed from SQL', function () {
|
|
// numbers and strings
|
|
this.db.function('a', (a, b, c) => a + b + c);
|
|
expect(this.get('a(?, ?, ?)', 2, 10, 50)).to.equal(62);
|
|
expect(this.get('a(?, ?, ?)', 2, 10, null)).to.equal(12);
|
|
expect(this.get('a(?, ?, ?)', 'foo', 'z', 12)).to.equal('fooz12');
|
|
|
|
// undefined is interpreted as null
|
|
this.db.function('b', (a, b) => null);
|
|
this.db.function('c', (a, b) => {});
|
|
expect(this.get('b(?, ?)', 2, 10)).to.equal(null);
|
|
expect(this.get('c(?, ?)', 2, 10)).to.equal(null);
|
|
|
|
// buffers
|
|
this.db.function('d', function foo(x) { return x; });
|
|
const input = Buffer.alloc(8).fill(0xdd);
|
|
const output = this.get('d(?)', input);
|
|
expect(input).to.not.equal(output);
|
|
expect(input.equals(output)).to.be.true;
|
|
expect(output.equals(Buffer.alloc(8).fill(0xdd))).to.be.true;
|
|
|
|
// should not register based on function.name
|
|
expect(() => this.get('foo(?)', input)).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
|
|
// zero arguments
|
|
this.db.function('e', () => 12);
|
|
expect(this.get('e()')).to.equal(12);
|
|
});
|
|
it('should use a strict number of arguments by default', function () {
|
|
this.db.function('fn', (a, b) => {});
|
|
expect(() => this.get('fn()')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
expect(() => this.get('fn(?)', 4)).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
expect(() => this.get('fn(?, ?, ?)', 4, 8, 3)).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
this.get('fn(?, ?)', 4, 8);
|
|
});
|
|
it('should accept a "varargs" option', function () {
|
|
const fn = (...args) => args.reduce((a, b) => a * b, 1);
|
|
Object.defineProperty(fn, 'length', { value: '-2' });
|
|
this.db.function('fn', { varargs: true }, fn);
|
|
expect(this.get('fn()')).to.equal(1);
|
|
expect(this.get('fn(?)', 7)).to.equal(7);
|
|
expect(this.get('fn(?, ?)', 4, 8)).to.equal(32);
|
|
expect(this.get('fn(?, ?, ?, ?, ?, ?)', 2, 3, 4, 5, 6, 7)).to.equal(5040);
|
|
});
|
|
it('should throw an exception if the database is busy', function () {
|
|
let ranOnce = false;
|
|
for (const x of this.db.prepare('SELECT 2').pluck().iterate()) {
|
|
expect(x).to.equal(2);
|
|
ranOnce = true;
|
|
expect(() => this.db.function('fn', () => {})).to.throw(TypeError);
|
|
}
|
|
expect(ranOnce).to.be.true;
|
|
this.db.function('fn', () => {});
|
|
});
|
|
it('should cause the database to become busy when executing the function', function () {
|
|
let ranOnce = false;
|
|
this.db.function('a', () => {});
|
|
this.db.function('b', () => {
|
|
ranOnce = true;
|
|
expect(() => this.db.exec('SELECT a()')).to.throw(TypeError);
|
|
expect(() => this.db.prepare('SELECT 555')).to.throw(TypeError);
|
|
expect(() => this.db.pragma('cache_size')).to.throw(TypeError);
|
|
expect(() => this.db.function('z', () => {})).to.throw(TypeError);
|
|
});
|
|
|
|
expect(this.get('b()')).to.equal(null);
|
|
expect(ranOnce).to.be.true;
|
|
ranOnce = false;
|
|
|
|
expect(this.db.exec('SELECT b()')).to.equal(this.db);
|
|
expect(ranOnce).to.be.true;
|
|
|
|
this.db.exec('SELECT a()')
|
|
this.db.prepare('SELECT 555');
|
|
this.db.pragma('cache_size');
|
|
this.db.function('zz', () => {});
|
|
});
|
|
it('should cause the function to throw when returning an invalid value', function () {
|
|
this.db.function('fn', x => ({}));
|
|
expect(() => this.get('fn(?)', 42)).to.throw(TypeError);
|
|
});
|
|
it('should propagate exceptions thrown in the registered function', function () {
|
|
const expectError = (name, exception) => {
|
|
this.db.function(name, () => { throw exception; });
|
|
try {
|
|
this.get(name + '()');
|
|
} catch (ex) {
|
|
expect(ex).to.equal(exception);
|
|
return;
|
|
}
|
|
throw new TypeError('Expected function to throw an exception');
|
|
};
|
|
expectError('a', new TypeError('foobar'));
|
|
expectError('b', new Error('baz'));
|
|
expectError('c', { yup: 'ok' });
|
|
expectError('d', 'foobarbazqux');
|
|
expectError('e', '');
|
|
expectError('f', null);
|
|
expectError('g', 123.4);
|
|
});
|
|
it('should close a statement iterator that caused its function to throw', function () {
|
|
this.db.prepare('CREATE TABLE iterable (value INTEGER)').run();
|
|
this.db.prepare('INSERT INTO iterable WITH RECURSIVE temp(x) AS (SELECT 1 UNION ALL SELECT x * 2 FROM temp LIMIT 10) SELECT * FROM temp').run();
|
|
|
|
let i = 0;
|
|
const err = new Error('foo');
|
|
this.db.function('fn', (x) => { if (++i >= 5) throw err; return x; });
|
|
const iterator = this.db.prepare('SELECT fn(value) FROM iterable').pluck().iterate();
|
|
|
|
let total = 0;
|
|
expect(() => {
|
|
for (const value of iterator) {
|
|
total += value;
|
|
expect(() => this.db.prepare('SELECT fn(value) FROM iterable')).to.throw(TypeError);
|
|
}
|
|
}).to.throw(err);
|
|
|
|
expect(total).to.equal(1 + 2 + 4 + 8);
|
|
expect(iterator.next()).to.deep.equal({ value: undefined, done: true });
|
|
this.db.prepare('SELECT fn(value) FROM iterable').pluck().iterate().return();
|
|
expect(total).to.equal(1 + 2 + 4 + 8);
|
|
});
|
|
it('should be able to register multiple functions with the same name', function () {
|
|
this.db.function('fn', () => 0);
|
|
this.db.function('fn', (a) => 1);
|
|
this.db.function('fn', (a, b) => 2);
|
|
this.db.function('fn', (a, b, c) => 3);
|
|
this.db.function('fn', (a, b, c, d) => 4);
|
|
expect(this.get('fn()')).to.equal(0);
|
|
expect(this.get('fn(555)')).to.equal(1);
|
|
expect(this.get('fn(555, 555)')).to.equal(2);
|
|
expect(this.get('fn(555, 555, 555)')).to.equal(3);
|
|
expect(this.get('fn(555, 555, 555, 555)')).to.equal(4);
|
|
this.db.function('fn', (a, b) => 'foobar');
|
|
expect(this.get('fn()')).to.equal(0);
|
|
expect(this.get('fn(555)')).to.equal(1);
|
|
expect(this.get('fn(555, 555)')).to.equal('foobar');
|
|
expect(this.get('fn(555, 555, 555)')).to.equal(3);
|
|
expect(this.get('fn(555, 555, 555, 555)')).to.equal(4);
|
|
});
|
|
it('should not be able to affect bound buffers mid-query', function () {
|
|
const input = Buffer.alloc(1024 * 8).fill(0xbb);
|
|
let ranOnce = false;
|
|
this.db.function('fn', () => {
|
|
ranOnce = true;
|
|
input[0] = 2;
|
|
});
|
|
const output = this.get('?, fn()', input);
|
|
expect(ranOnce).to.be.true;
|
|
expect(output.equals(Buffer.alloc(1024 * 8).fill(0xbb))).to.be.true;
|
|
});
|
|
describe('should not affect external environment', function () {
|
|
specify('busy state', function () {
|
|
this.db.function('fn', (x) => {
|
|
expect(() => this.db.prepare('SELECT 555')).to.throw(TypeError);
|
|
return x * 2;
|
|
});
|
|
let ranOnce = false;
|
|
for (const x of this.db.prepare('SELECT fn(555)').pluck().iterate()) {
|
|
ranOnce = true;
|
|
expect(x).to.equal(1110);
|
|
expect(() => this.db.prepare('SELECT 555')).to.throw(TypeError);
|
|
}
|
|
expect(ranOnce).to.be.true;
|
|
this.db.prepare('SELECT 555');
|
|
});
|
|
specify('was_js_error state', function () {
|
|
this.db.prepare('CREATE TABLE data (value INTEGER)').run();
|
|
const stmt = this.db.prepare('SELECT value FROM data');
|
|
this.db.prepare('DROP TABLE data').run();
|
|
|
|
const err = new Error('foo');
|
|
this.db.function('fn', () => { throw err; });
|
|
|
|
expect(() => this.db.prepare('SELECT fn()').get()).to.throw(err);
|
|
try { stmt.get(); } catch (ex) {
|
|
expect(ex).to.be.an.instanceof(Error);
|
|
expect(ex).to.not.equal(err);
|
|
expect(ex.message).to.not.equal(err.message);
|
|
expect(ex).to.be.an.instanceof(Database.SqliteError);
|
|
return;
|
|
}
|
|
throw new TypeError('Expected the statement to throw an exception');
|
|
});
|
|
});
|
|
});
|