600 lines
27 KiB
JavaScript
600 lines
27 KiB
JavaScript
'use strict';
|
|
const Database = require('../.');
|
|
|
|
describe('Database#aggregate()', function () {
|
|
beforeEach(function () {
|
|
this.db = new Database(util.next());
|
|
this.db.prepare('CREATE TABLE empty (_)').run();
|
|
this.db.prepare('CREATE TABLE ints (_)').run();
|
|
this.db.prepare('CREATE TABLE texts (_)').run();
|
|
this.db.prepare('INSERT INTO ints VALUES (?), (?), (?), (?), (?), (?), (?)').run(3, 5, 7, 11, 13, 17, 19);
|
|
this.db.prepare('INSERT INTO texts VALUES (?), (?), (?), (?), (?), (?), (?)').run('a', 'b', 'c', 'd', 'e', 'f', 'g');
|
|
this.get = (SQL, ...args) => this.db.prepare(`SELECT ${SQL}`).pluck().get(args);
|
|
this.all = (SQL, ...args) => this.db.prepare(`SELECT ${SQL} WINDOW win AS (ORDER BY rowid ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) ORDER BY rowid`).pluck().all(args);
|
|
});
|
|
afterEach(function () {
|
|
this.db.close();
|
|
});
|
|
|
|
it('should throw an exception if the correct arguments are not provided', function () {
|
|
expect(() => this.db.aggregate()).to.throw(TypeError);
|
|
expect(() => this.db.aggregate(null)).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('a')).to.throw(TypeError);
|
|
expect(() => this.db.aggregate({})).to.throw(TypeError);
|
|
expect(() => this.db.aggregate({ step: () => {} })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate({ name: 'b', step: function b() {} })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate(() => {})).to.throw(TypeError);
|
|
expect(() => this.db.aggregate(function c() {})).to.throw(TypeError);
|
|
expect(() => this.db.aggregate({}, function d() {})).to.throw(TypeError);
|
|
expect(() => this.db.aggregate({ name: 'e', step: function e() {} }, function e() {})).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('f')).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('g', null)).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('h', {})).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('i', function i() {})).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('j', {}, function j() {})).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('k', { name: 'k' }, function k() {})).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('l', { inverse: function l() {} })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('m', { result: function m() {} })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate(new String('n'), { step: function n() {} })).to.throw(TypeError);
|
|
});
|
|
it('should throw an exception if boolean options are provided as non-booleans', function () {
|
|
expect(() => this.db.aggregate('a', { step: () => {}, varargs: undefined })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('b', { step: () => {}, deterministic: undefined })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('c', { step: () => {}, safeIntegers: undefined })).to.throw(TypeError);
|
|
});
|
|
it('should throw an exception if function options are provided as non-fns', function () {
|
|
expect(() => this.db.aggregate('a', { step: undefined })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('b', { step: null })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('c', { step: false })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('d', { step: true })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('e', { step: Object.create(Function.prototype) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('f', { step: () => {}, inverse: false })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('g', { step: () => {}, inverse: true })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('h', { step: () => {}, inverse: Object.create(Function.prototype) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('i', { step: () => {}, result: false })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('j', { step: () => {}, result: true })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('k', { step: () => {}, result: Object.create(Function.prototype) })).to.throw(TypeError);
|
|
});
|
|
it('should throw an exception if the provided name is empty', function () {
|
|
expect(() => this.db.aggregate('', { step: () => {} })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('', { name: 'a', step: () => {} })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('', { name: 'b', step: function b() {} })).to.throw(TypeError);
|
|
});
|
|
it('should throw an exception if step.length or inverse.length is invalid', function () {
|
|
const length = x => Object.defineProperty(() => {}, 'length', { value: x });
|
|
expect(() => this.db.aggregate('a', { step: length(undefined) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('b', { step: length(null) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('c', { step: length('2') })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('d', { step: length(NaN) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('e', { step: length(Infinity) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('f', { step: length(2.000000001) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('g', { step: length(-0.000000001) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('h', { step: length(-2) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('i', { step: length(100.000000001) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('j', { step: length(102) })).to.throw(RangeError);
|
|
expect(() => this.db.aggregate('aa', { step: () => {}, inverse: length(undefined) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('bb', { step: () => {}, inverse: length(null) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('cc', { step: () => {}, inverse: length('2') })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('dd', { step: () => {}, inverse: length(NaN) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('ee', { step: () => {}, inverse: length(Infinity) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('ff', { step: () => {}, inverse: length(2.000000001) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('gg', { step: () => {}, inverse: length(-0.000000001) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('hh', { step: () => {}, inverse: length(-2) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('ii', { step: () => {}, inverse: length(100.000000001) })).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('jj', { step: () => {}, inverse: length(102) })).to.throw(RangeError);
|
|
});
|
|
it('should register an aggregate function and return the database object', function () {
|
|
const length = x => Object.defineProperty(() => {}, 'length', { value: x });
|
|
expect(this.db.aggregate('a', { step: () => {} })).to.equal(this.db);
|
|
expect(this.db.aggregate('b', { step: function x() {} })).to.equal(this.db);
|
|
expect(this.db.aggregate('c', { step: length(1) })).to.equal(this.db);
|
|
expect(this.db.aggregate('d', { step: length(101) })).to.equal(this.db);
|
|
});
|
|
it('should enable the registered aggregate function to be executed from SQL', function () {
|
|
// numbers
|
|
this.db.aggregate('a', { step: (ctx, a, b) => a * b + ctx });
|
|
expect(this.get('a(_, ?) FROM ints', 2)).to.equal(150);
|
|
|
|
// strings
|
|
this.db.aggregate('b', { step: (ctx, a, b) => a + b + ctx });
|
|
expect(this.get('b(_, ?) FROM texts', '!')).to.equal('g!f!e!d!c!b!a!null');
|
|
|
|
// starting value is null
|
|
this.db.aggregate('c', { step: (ctx, x) => null });
|
|
this.db.aggregate('d', { step: (ctx, x) => ctx });
|
|
this.db.aggregate('e', { step: (ctx, x) => {} });
|
|
expect(this.get('c(_) FROM ints')).to.equal(null);
|
|
expect(this.get('d(_) FROM ints')).to.equal(null);
|
|
expect(this.get('e(_) FROM ints')).to.equal(null);
|
|
|
|
// buffers
|
|
this.db.aggregate('f', { step: (ctx, x) => x });
|
|
const input = Buffer.alloc(8).fill(0xdd);
|
|
const output = this.get('f(?)', 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;
|
|
|
|
// zero arguments
|
|
this.db.aggregate('g', { step: (ctx) => 'z' + ctx });
|
|
this.db.aggregate('h', { step: (ctx) => 12 });
|
|
this.db.aggregate('i', { step: () => 44 });
|
|
expect(this.get('g()')).to.equal('znull');
|
|
expect(this.get('h()')).to.equal(12);
|
|
expect(this.get('i()')).to.equal(44);
|
|
expect(this.get('g() FROM empty')).to.equal(null);
|
|
expect(this.get('h() FROM empty')).to.equal(null);
|
|
expect(this.get('i() FROM empty')).to.equal(null);
|
|
expect(this.get('g() FROM ints')).to.equal('zzzzzzznull');
|
|
expect(this.get('h() FROM ints')).to.equal(12);
|
|
expect(this.get('i() FROM ints')).to.equal(44);
|
|
expect(this.get('g(*) FROM ints')).to.equal('zzzzzzznull');
|
|
expect(this.get('h(*) FROM ints')).to.equal(12);
|
|
expect(this.get('i(*) FROM ints')).to.equal(44);
|
|
});
|
|
it('should use a strict number of arguments by default', function () {
|
|
this.db.aggregate('agg', { step: (ctx, a, b) => {} });
|
|
expect(() => this.get('agg()')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
expect(() => this.get('agg(?)', 4)).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
expect(() => this.get('agg(?, ?, ?)', 4, 8, 3)).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
this.get('agg(?, ?)', 4, 8);
|
|
});
|
|
it('should accept a "varargs" option', function () {
|
|
const step = (ctx, ...args) => args.reduce((a, b) => a * b, 1) + ctx;
|
|
Object.defineProperty(step, 'length', { value: '-2' });
|
|
this.db.aggregate('agg', { varargs: true, step });
|
|
expect(this.get('agg()')).to.equal(1);
|
|
expect(this.get('agg(?)', 7)).to.equal(7);
|
|
expect(this.get('agg(?, ?)', 4, 8)).to.equal(32);
|
|
expect(this.get('agg(?, ?, ?, ?, ?, ?)', 2, 3, 4, 5, 6, 7)).to.equal(5040);
|
|
});
|
|
it('should accept an optional start value', function () {
|
|
this.db.aggregate('a', { start: 10000, step: (ctx, a, b) => a * b + ++ctx });
|
|
expect(this.get('a(_, ?) FROM ints', 2)).to.equal(10157);
|
|
expect(this.get('a(_, ?) FROM ints', 2)).to.equal(10157);
|
|
|
|
this.db.aggregate('b', { start: { foo: 1000 }, step: (ctx, a, b) => a * b + (ctx.foo ? ++ctx.foo : ++ctx) });
|
|
expect(this.get('b(_, ?) FROM ints', 2)).to.equal(1157);
|
|
expect(this.get('b(_, ?) FROM ints', 2)).to.equal(1158);
|
|
|
|
let ranOnce = false;
|
|
this.db.aggregate('c', { start: undefined, step: (ctx, a, b) => {
|
|
if (ranOnce) expect(ctx).to.be.NaN;
|
|
else expect(ctx).to.be.undefined;
|
|
ranOnce = true;
|
|
return a * b + ++ctx;
|
|
} });
|
|
expect(this.get('c(_, ?) FROM ints', 2)).to.equal(null);
|
|
expect(ranOnce).to.be.true;
|
|
ranOnce = false;
|
|
expect(this.get('c(_, ?) FROM ints', 2)).to.equal(null);
|
|
expect(ranOnce).to.be.true;
|
|
});
|
|
it('should accept an optional start() function', function () {
|
|
let start = 10000;
|
|
|
|
this.db.aggregate('a', { start: () => start++, step: (ctx, a, b) => a * b + ctx });
|
|
expect(this.get('a(_, ?) FROM ints', 2)).to.equal(10150);
|
|
expect(this.get('a(_, ?) FROM ints', 2)).to.equal(10151);
|
|
expect(this.get('a(_, ?) FROM ints', 2)).to.equal(10152);
|
|
|
|
this.db.aggregate('b', { start: () => ({ foo: start-- }), step: (ctx, a, b) => a * b + (ctx.foo || ctx) });
|
|
expect(this.get('b(_, ?) FROM ints', 2)).to.equal(10153);
|
|
expect(this.get('b(_, ?) FROM ints', 2)).to.equal(10152);
|
|
expect(this.get('b(_, ?) FROM ints', 2)).to.equal(10151);
|
|
|
|
let ranOnce = false;
|
|
this.db.aggregate('c', { start: () => undefined, step: (ctx, a, b) => {
|
|
if (ranOnce) expect(ctx).to.be.NaN;
|
|
else expect(ctx).to.be.undefined;
|
|
ranOnce = true;
|
|
return a * b + ++ctx;
|
|
} });
|
|
expect(this.get('c(_, ?) FROM ints', 2)).to.equal(null);
|
|
expect(ranOnce).to.be.true;
|
|
ranOnce = false;
|
|
expect(this.get('c(_, ?) FROM ints', 2)).to.equal(null);
|
|
expect(ranOnce).to.be.true;
|
|
});
|
|
it('should not change the aggregate value when step() returns undefined', function () {
|
|
this.db.aggregate('a', { start: 10000, step: (ctx, a, b) => a === 11 ? undefined : a * b + ctx });
|
|
expect(this.get('a(_, ?) FROM ints', 2)).to.equal(10128);
|
|
this.db.aggregate('b', { start: () => 1000, step: (ctx, a, b) => {} });
|
|
expect(this.get('b(_, ?) FROM ints', 2)).to.equal(1000);
|
|
this.db.aggregate('c', { start: () => 1000, step: (ctx, a, b) => null });
|
|
expect(this.get('c(_, ?) FROM ints', 2)).to.equal(null);
|
|
});
|
|
it('should accept a result() transformer function', function () {
|
|
this.db.aggregate('a', {
|
|
start: 10000,
|
|
step: (ctx, a, b) => a * b + ctx,
|
|
result: ctx => ctx / 2,
|
|
});
|
|
expect(this.get('a(_, ?) FROM ints', 2)).to.equal(5075);
|
|
this.db.aggregate('b', {
|
|
start: () => ({ foo: 1000 }),
|
|
step: (ctx, a, b) => { ctx.foo += a * b; },
|
|
result: ctx => ctx.foo,
|
|
});
|
|
expect(this.get('b(_, ?) FROM ints', 2)).to.equal(1150);
|
|
expect(this.get('b(_, ?) FROM ints', 2)).to.equal(1150); // should play well when ran multiple times
|
|
this.db.aggregate('c', {
|
|
start: () => ({ foo: 1000 }),
|
|
step: (ctx, a, b) => { ctx.foo += 1; },
|
|
result: ctx => ctx.foo,
|
|
});
|
|
expect(this.get('c(_, ?) FROM empty', 2)).to.equal(1000);
|
|
});
|
|
it('should interpret undefined as null within a result() function', function () {
|
|
this.db.aggregate('agg', {
|
|
start: 10000,
|
|
step: (ctx, a, b) => a * b + ctx,
|
|
result: () => {},
|
|
});
|
|
expect(this.get('agg(_, ?) FROM ints', 2)).to.equal(null);
|
|
});
|
|
it('should accept an inverse() function to support aggregate window functions', function () {
|
|
this.db.aggregate('agg', {
|
|
start: () => 10000,
|
|
step: (ctx, a, b) => a * b + ctx,
|
|
});
|
|
expect(() => this.all('agg(_, ?) OVER win FROM ints', 2))
|
|
.to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
this.db.aggregate('wn', {
|
|
start: () => 10000,
|
|
step: (ctx, a, b) => a * b + ctx,
|
|
inverse: (ctx, a, b) => ctx - a * b,
|
|
});
|
|
expect(this.all('wn(_, ?) OVER win FROM ints', 2))
|
|
.to.deep.equal([10016, 10030, 10046, 10062, 10082, 10098, 10072]);
|
|
});
|
|
it('should not change the aggregate value when inverse() returns undefined', function () {
|
|
this.db.aggregate('a', {
|
|
start: () => 10000,
|
|
step: (ctx, a, b) => a * b + ctx,
|
|
inverse: (ctx, a, b) => a === 11 ? undefined : ctx - a * b,
|
|
});
|
|
expect(this.all('a(_, ?) OVER win FROM ints', 2))
|
|
.to.deep.equal([10016, 10030, 10046, 10062, 10082, 10120, 10094]);
|
|
this.db.aggregate('b', {
|
|
start: () => 10000,
|
|
step: (ctx, a, b) => ctx ? a * b + ctx : null,
|
|
inverse: (ctx, a, b) => null,
|
|
});
|
|
expect(this.all('b(_, ?) OVER win FROM ints', 2))
|
|
.to.deep.equal([10016, 10030, null, null, null, null, null]);
|
|
});
|
|
it('should potentially call result() multiple times for window functions', function () {
|
|
let startCount = 0;
|
|
let stepCount = 0;
|
|
let inverseCount = 0;
|
|
let resultCount = 0;
|
|
this.db.aggregate('wn', {
|
|
start: () => {
|
|
startCount += 1;
|
|
return { foo: 1000, results: 0 };
|
|
},
|
|
step: (ctx, a, b) => {
|
|
stepCount += 1;
|
|
ctx.foo += a * b;
|
|
},
|
|
inverse: (ctx, a, b) => {
|
|
inverseCount += 1;
|
|
ctx.foo -= a * b;
|
|
},
|
|
result: (ctx) => {
|
|
resultCount += 1;
|
|
return ctx.foo + ctx.results++ * 10000;
|
|
},
|
|
});
|
|
expect(this.all('wn(_, ?) OVER win FROM ints', 2))
|
|
.to.deep.equal([1016, 11030, 21046, 31062, 41082, 51098, 61072]);
|
|
expect(startCount).to.equal(1);
|
|
expect(stepCount).to.equal(7);
|
|
expect(inverseCount).to.equal(5);
|
|
expect(resultCount).to.equal(7);
|
|
expect(this.all('wn(_, ?) OVER win FROM ints', 2)) // should play well when ran multiple times
|
|
.to.deep.equal([1016, 11030, 21046, 31062, 41082, 51098, 61072]);
|
|
expect(startCount).to.equal(2);
|
|
expect(stepCount).to.equal(14);
|
|
expect(inverseCount).to.equal(10);
|
|
expect(resultCount).to.equal(14);
|
|
expect(this.all('wn(_, ?) OVER win FROM empty', 2))
|
|
.to.deep.equal([]);
|
|
expect(startCount).to.equal(2);
|
|
expect(stepCount).to.equal(14);
|
|
expect(inverseCount).to.equal(10);
|
|
expect(resultCount).to.equal(14);
|
|
});
|
|
it('should infer argument count from the greater of step() and inverse()', function () {
|
|
this.db.aggregate('a', {
|
|
start: () => 10000,
|
|
step: (ctx, a) => a + ctx,
|
|
inverse: (ctx, a, b) => ctx - a,
|
|
});
|
|
expect(this.all('a(_, ?) OVER win FROM ints', 2))
|
|
.to.deep.equal([10008, 10015, 10023, 10031, 10041, 10049, 10036]);
|
|
expect(() => this.all('a(_) OVER win FROM ints'))
|
|
.to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
this.db.aggregate('b', {
|
|
start: () => 10000,
|
|
step: (ctx, a, b) => a + ctx,
|
|
inverse: (ctx, a) => ctx - a,
|
|
});
|
|
expect(this.all('b(_, ?) OVER win FROM ints', 2))
|
|
.to.deep.equal([10008, 10015, 10023, 10031, 10041, 10049, 10036]);
|
|
expect(() => this.all('b(_) OVER win FROM ints'))
|
|
.to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
this.db.aggregate('c', {
|
|
start: (a, b, c, d, e) => 10000,
|
|
step: () => {},
|
|
inverse: (ctx, a) => --ctx,
|
|
result: (ctx, a, b, c, d, e) => ctx,
|
|
});
|
|
expect(this.all('c(_) OVER win FROM ints'))
|
|
.to.deep.equal([10000, 10000, 9999, 9998, 9997, 9996, 9995]);
|
|
expect(() => this.all('c() OVER win FROM ints'))
|
|
.to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
expect(() => this.all('c(*) OVER win FROM ints'))
|
|
.to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
expect(() => this.all('c(_, ?) OVER win FROM ints', 2))
|
|
.to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
|
|
});
|
|
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.aggregate('a', { step: () => {} })).to.throw(TypeError);
|
|
}
|
|
expect(ranOnce).to.be.true;
|
|
this.db.aggregate('b', { step: () => {} });
|
|
});
|
|
it('should cause the database to become busy when executing the aggregate', function () {
|
|
let checkCount = 0;
|
|
const expectBusy = () => {
|
|
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('x', () => {})).to.throw(TypeError);
|
|
expect(() => this.db.aggregate('y', { step: () => {} })).to.throw(TypeError);
|
|
checkCount += 1;
|
|
};
|
|
this.db.aggregate('a', { step: () => {} });
|
|
this.db.aggregate('b', { start: expectBusy, step: expectBusy, inverse: expectBusy, result: expectBusy });
|
|
|
|
expect(this.all('b(*) OVER win FROM ints')).to.deep.equal([null, null, null, null, null, null, null]);
|
|
expect(checkCount).to.equal(20);
|
|
checkCount = 0;
|
|
|
|
expect(this.db.exec('SELECT b(*) OVER win FROM ints WINDOW win AS (ORDER BY rowid ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) ORDER BY rowid')).to.equal(this.db);
|
|
expect(checkCount).to.equal(20);
|
|
|
|
this.db.exec('SELECT a()');
|
|
this.db.prepare('SELECT 555');
|
|
this.db.pragma('cache_size');
|
|
this.db.function('xx', () => {});
|
|
this.db.aggregate('yy', { step: () => {} });
|
|
});
|
|
it('should cause the aggregate to throw when returning an invalid value', function () {
|
|
this.db.aggregate('a', {
|
|
start: () => ({}),
|
|
step: () => ({}),
|
|
inverse: () => ({}),
|
|
result: () => 42,
|
|
});
|
|
this.db.aggregate('b', {
|
|
start: () => 42,
|
|
step: () => 42,
|
|
inverse: () => 42,
|
|
result: () => ({}),
|
|
});
|
|
this.db.aggregate('c', {
|
|
step: () => {},
|
|
result: () => 42,
|
|
});
|
|
this.db.aggregate('d', {
|
|
step: () => {},
|
|
result: () => ({}),
|
|
});
|
|
expect(this.all('a(*) OVER win FROM ints')).to.deep.equal([42, 42, 42, 42, 42, 42, 42]);
|
|
expect(() => this.all('b(*) OVER win FROM ints')).to.throw(TypeError);
|
|
expect(this.get('c(*) FROM ints')).to.equal(42);
|
|
expect(this.get('c(*) FROM empty')).to.equal(42);
|
|
expect(() => this.get('d(*) FROM ints')).to.throw(TypeError);
|
|
expect(() => this.get('d(*) FROM empty')).to.throw(TypeError);
|
|
});
|
|
it('should close a statement iterator that caused its aggregate 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.aggregate('wn', {
|
|
step: (ctx, x) => { if (++i >= 5) throw err; return x; },
|
|
inverse: () => {},
|
|
});
|
|
const iterator = this.db.prepare('SELECT wn(value) OVER (ROWS CURRENT ROW) FROM iterable').pluck().iterate();
|
|
|
|
let total = 0;
|
|
expect(() => {
|
|
for (const value of iterator) {
|
|
total += value;
|
|
expect(() => this.db.prepare('SELECT wn(value) OVER (ROWS CURRENT ROW) 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 wn(value) OVER (ROWS CURRENT ROW) FROM iterable').pluck().iterate().return();
|
|
expect(total).to.equal(1 + 2 + 4 + 8);
|
|
});
|
|
it('should be able to register multiple aggregates with the same name', function () {
|
|
this.db.aggregate('agg', { step: (ctx) => 0 });
|
|
this.db.aggregate('agg', { step: (ctx, a) => 1 });
|
|
this.db.aggregate('agg', { step: (ctx, a, b) => 2 });
|
|
this.db.aggregate('agg', { step: (ctx, a, b, c) => 3, inverse: () => {} });
|
|
this.db.aggregate('agg', { step: (ctx, a, b, c, d) => 4 });
|
|
expect(this.get('agg()')).to.equal(0);
|
|
expect(this.get('agg(555)')).to.equal(1);
|
|
expect(this.get('agg(555, 555)')).to.equal(2);
|
|
expect(this.get('agg(555, 555, 555)')).to.equal(3);
|
|
expect(this.get('agg(555, 555, 555, 555)')).to.equal(4);
|
|
this.db.aggregate('agg', { step: (ctx, a, b) => 'foo', inverse: () => {} });
|
|
this.db.aggregate('agg', { step: (ctx, a, b, c) => 'bar' });
|
|
expect(this.get('agg()')).to.equal(0);
|
|
expect(this.get('agg(555)')).to.equal(1);
|
|
expect(this.get('agg(555, 555)')).to.equal('foo');
|
|
expect(this.get('agg(555, 555, 555)')).to.equal('bar');
|
|
expect(this.get('agg(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 startCalled = false;
|
|
let stepCalled = false;
|
|
this.db.aggregate('agg', {
|
|
start: () => {
|
|
startCalled = true;
|
|
input[0] = 2;
|
|
},
|
|
step: () => {
|
|
stepCalled = true;
|
|
input[0] = 2;
|
|
},
|
|
});
|
|
const output = this.get('?, agg(*) FROM ints', input);
|
|
expect(startCalled).to.be.true;
|
|
expect(stepCalled).to.be.true;
|
|
expect(output.equals(Buffer.alloc(1024 * 8).fill(0xbb))).to.be.true;
|
|
});
|
|
describe('should propagate exceptions', function () {
|
|
const exceptions = [new TypeError('foobar'), new Error('baz'), { yup: 'ok' }, 'foobarbazqux', '', null, 123.4];
|
|
const expectError = (exception, fn) => {
|
|
try { fn(); } catch (ex) {
|
|
expect(ex).to.equal(exception);
|
|
return;
|
|
}
|
|
throw new TypeError('Expected aggregate to throw an exception');
|
|
};
|
|
|
|
specify('thrown in the start() function', function () {
|
|
exceptions.forEach((exception, index) => {
|
|
const calls = [];
|
|
this.db.aggregate(`wn${index}`, {
|
|
start: () => { calls.push('a'); throw exception; },
|
|
step: () => { calls.push('b'); },
|
|
inverse: () => { calls.push('c'); },
|
|
result: () => { calls.push('d'); },
|
|
});
|
|
expectError(exception, () => this.get(`wn${index}() FROM empty`));
|
|
expect(calls.splice(0)).to.deep.equal(['a']);
|
|
expectError(exception, () => this.get(`wn${index}() FROM ints`));
|
|
expect(calls.splice(0)).to.deep.equal(['a']);
|
|
expectError(exception, () => this.all(`wn${index}() OVER win FROM ints`));
|
|
expect(calls.splice(0)).to.deep.equal(['a']);
|
|
});
|
|
});
|
|
specify('thrown in the step() function', function () {
|
|
exceptions.forEach((exception, index) => {
|
|
const calls = [];
|
|
this.db.aggregate(`wn${index}`, {
|
|
start: () => { calls.push('a'); },
|
|
step: () => { calls.push('b'); throw exception; },
|
|
inverse: () => { calls.push('c'); },
|
|
result: () => { calls.push('d'); },
|
|
});
|
|
expect(this.get(`wn${index}() FROM empty`)).to.equal(null);
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'd']);
|
|
expectError(exception, () => this.get(`wn${index}() FROM ints`));
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'b']);
|
|
expectError(exception, () => this.all(`wn${index}() OVER win FROM ints`));
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'b']);
|
|
});
|
|
});
|
|
specify('thrown in the inverse() function', function () {
|
|
exceptions.forEach((exception, index) => {
|
|
const calls = [];
|
|
this.db.aggregate(`wn${index}`, {
|
|
start: () => { calls.push('a'); },
|
|
step: () => { calls.push('b'); },
|
|
inverse: () => { calls.push('c'); throw exception; },
|
|
result: () => { calls.push('d'); },
|
|
});
|
|
expect(this.get(`wn${index}() FROM empty`)).to.equal(null);
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'd']);
|
|
expect(this.get(`wn${index}() FROM ints`)).to.equal(null);
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'd']);
|
|
expectError(exception, () => this.all(`wn${index}() OVER win FROM ints`));
|
|
expect(calls.length).to.be.above(2);
|
|
expect(calls.indexOf('c')).to.equal(calls.length - 1);
|
|
});
|
|
});
|
|
specify('thrown in the result() function', function () {
|
|
exceptions.forEach((exception, index) => {
|
|
const calls = [];
|
|
this.db.aggregate(`wn${index}`, {
|
|
start: () => { calls.push('a'); },
|
|
step: () => { calls.push('b'); },
|
|
inverse: () => { calls.push('c'); },
|
|
result: () => { calls.push('d'); throw exception; },
|
|
});
|
|
expectError(exception, () => this.get(`wn${index}() FROM empty`));
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'd']);
|
|
expectError(exception, () => this.get(`wn${index}() FROM ints`));
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'd']);
|
|
expectError(exception, () => this.all(`wn${index}() OVER win FROM ints`));
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'b', 'b', 'd']);
|
|
});
|
|
});
|
|
specify('thrown due to returning an invalid value', function () {
|
|
const calls = [];
|
|
this.db.aggregate('wn', {
|
|
start: () => { calls.push('a'); },
|
|
step: () => { calls.push('b'); },
|
|
inverse: () => { calls.push('c'); },
|
|
result: () => { calls.push('d'); return {}; },
|
|
});
|
|
expect(() => this.get('wn() FROM empty')).to.throw(TypeError);
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'd']);
|
|
expect(() => this.get('wn() FROM ints')).to.throw(TypeError);;
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'd']);
|
|
expect(() => this.all('wn() OVER win FROM ints')).to.throw(TypeError);;
|
|
expect(calls.splice(0)).to.deep.equal(['a', 'b', 'b', 'd']);
|
|
});
|
|
});
|
|
describe('should not affect external environment', function () {
|
|
specify('busy state', function () {
|
|
this.db.aggregate('agg', { step: (ctx, x) => {
|
|
expect(() => this.db.prepare('SELECT 555')).to.throw(TypeError);
|
|
return x * 2 + ctx;
|
|
} });
|
|
let ranOnce = false;
|
|
for (const x of this.db.prepare('SELECT agg(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.aggregate('agg', { step: () => { throw err; } });
|
|
|
|
expect(() => this.db.prepare('SELECT agg()').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');
|
|
});
|
|
});
|
|
});
|