'use strict'; const Database = require('../.'); describe('Database#table()', function () { beforeEach(function () { this.db = new Database(util.next()); }); afterEach(function () { this.db.close(); }); it('should throw an exception if the correct arguments are not provided', function () { expect(() => this.db.table()).to.throw(TypeError); expect(() => this.db.table(null)).to.throw(TypeError); expect(() => this.db.table('a')).to.throw(TypeError); expect(() => this.db.table({})).to.throw(TypeError); expect(() => this.db.table({ rows: function*(){}, columns: ['x'] })).to.throw(TypeError); expect(() => this.db.table({ name: 'b', rows: function*(){}, columns: ['x'] })).to.throw(TypeError); expect(() => this.db.table(() => {})).to.throw(TypeError); expect(() => this.db.table(function* c() {})).to.throw(TypeError); expect(() => this.db.table({}, function d() {})).to.throw(TypeError); expect(() => this.db.table({ name: 'e', rows: function* e() {}, columns: ['x'] }, function e() {})).to.throw(TypeError); expect(() => this.db.table('f')).to.throw(TypeError); expect(() => this.db.table('g', null)).to.throw(TypeError); expect(() => this.db.table('h', {})).to.throw(TypeError); expect(() => this.db.table('i', Object.create(Function.prototype))).to.throw(TypeError); expect(() => this.db.table('j', { columns: ['x'] }, function j() {})).to.throw(TypeError); expect(() => this.db.table('k', { name: 'k', columns: ['x'] }, function* k() {})).to.throw(TypeError); expect(() => this.db.table('l', { name: 'l', rows: function* l() {} })).to.throw(TypeError); expect(() => this.db.table(new String('m'), { columns: ['x'], rows: function* m() {} })).to.throw(TypeError); expect(() => this.db.table(new String('n'), () => {})).to.throw(TypeError); }); it('should throw an exception if boolean options are provided as non-booleans', function () { expect(() => this.db.table('a', { columns: ['x'], rows: function*(){}, directOnly: undefined })).to.throw(TypeError); expect(() => this.db.table('b', { columns: ['x'], rows: function*(){}, safeIntegers: undefined })).to.throw(TypeError); }); it('should throw an exception if the "columns" option is invalid', function () { expect(() => this.db.table('a', { rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('b', { columns: undefined, rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('c', { columns: 'x', rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('d', { columns: { length: 1, 0: 'x', [Symbol.iterator]: () => ['x'].values() }, rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('e', { columns: ['x',, 'y'], rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('f', { columns: ['x', new String('y')], rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('g', { columns: ['x', 'x'], rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('h', { columns: [], rows: function*(){} })).to.throw(RangeError); }); it('should throw an exception if the "parameters" option is invalid', function () { expect(() => this.db.table('a', { parameters: undefined, columns: ['foo'], rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('b', { parameters: 'x', columns: ['foo'], rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('c', { parameters: { length: 1, 0: 'x', [Symbol.iterator]: () => ['x'].values() }, columns: ['foo'], rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('d', { parameters: ['x',, 'y'], columns: ['foo'], rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('e', { parameters: ['x', new String('y')], columns: ['foo'], rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('f', { parameters: ['x', 'x'], columns: ['foo'], rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('g', { parameters: ['x'], columns: ['x'], rows: function*(){} })).to.throw(TypeError); expect(() => this.db.table('h', { parameters: [...Array(33)].map((_, i) => `p${i}`), columns: ['foo'], rows: function*(){} })).to.throw(RangeError); }); it('should throw an exception if the "rows" option is invalid', function () { expect(() => this.db.table('a', { columns: ['x'] })).to.throw(TypeError); expect(() => this.db.table('b', { columns: ['x'], rows: undefined })).to.throw(TypeError); expect(() => this.db.table('c', { columns: ['x'], rows: {} })).to.throw(TypeError); expect(() => this.db.table('d', { columns: ['x'], rows: () => {} })).to.throw(TypeError); expect(() => this.db.table('e', { columns: ['x'], rows: function () {} })).to.throw(TypeError); expect(() => this.db.table('f', { columns: ['x'], rows: Object.create(Function.prototype) })).to.throw(TypeError); expect(() => this.db.table('g', { columns: ['x'], rows: Object.create(Object.getPrototypeOf(function*(){})) })).to.throw(TypeError); expect(() => this.db.table('h', { columns: ['x'], rows: Object.setPrototypeOf(() => {}, Object.create(Object.getPrototypeOf(function*(){}))) })).to.throw(TypeError); }); it('should throw an exception if the provided name is empty', function () { expect(() => this.db.table('', { columns: ['x'], rows: function* () {} })).to.throw(TypeError); expect(() => this.db.table('', { name: 'a', columns: ['x'], rows: function* () {} })).to.throw(TypeError); expect(() => this.db.table('', { name: 'b', columns: ['x'], rows: function* b() {} })).to.throw(TypeError); expect(() => this.db.table('', function c() {})).to.throw(TypeError); }); it('should throw an exception if generator.length is invalid', function () { const length = x => Object.defineProperty(function*(){}, 'length', { value: x }); expect(() => this.db.table('a', { columns: ['x'], rows: length(undefined) })).to.throw(TypeError); expect(() => this.db.table('b', { columns: ['x'], rows: length(null) })).to.throw(TypeError); expect(() => this.db.table('c', { columns: ['x'], rows: length('1') })).to.throw(TypeError); expect(() => this.db.table('d', { columns: ['x'], rows: length(NaN) })).to.throw(TypeError); expect(() => this.db.table('e', { columns: ['x'], rows: length(Infinity) })).to.throw(TypeError); expect(() => this.db.table('f', { columns: ['x'], rows: length(1.000000001) })).to.throw(TypeError); expect(() => this.db.table('g', { columns: ['x'], rows: length(-0.000000001) })).to.throw(TypeError); expect(() => this.db.table('h', { columns: ['x'], rows: length(-1) })).to.throw(TypeError); expect(() => this.db.table('i', { columns: ['x'], rows: length(32.000000001) })).to.throw(TypeError); expect(() => this.db.table('j', { columns: ['x'], rows: length(33) })).to.throw(RangeError); }); it('should register a virtual table and return the database object', function () { const length = x => Object.defineProperty(function*(){}, 'length', { value: x }); expect(this.db.table('a', { columns: ['x'], rows: function* () {} })).to.equal(this.db); expect(this.db.table('b', { columns: ['x'], rows: length(1) })).to.equal(this.db); expect(this.db.table('c', { columns: ['x'], rows: length(32) })).to.equal(this.db); }); it('should enable the registered virtual table to be queried from SQL', function () { const rows = [ { a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') }, { a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') }, ]; this.db.table('vtab', { columns: ['a', 'b', 'c', 'd', 'e'], *rows() { for (const obj of rows) { yield Object.values(obj); } }, }); expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows); expect(this.db.prepare('SELECT * FROM vtab WHERE b < 500').all()).to.deep.equal(rows.slice(0, 1)); expect(this.db.prepare('SELECT * FROM vtab ORDER BY d DESC').all()).to.deep.equal(rows.slice().reverse()); }); it('should infer parameters for the virtual table', function () { this.db.table('vtab', { columns: ['a', 'b'], *rows(x, y) { yield [x, y]; yield [x * 2, y * 3]; }, }); expect(this.db.prepare('SELECT * FROM vtab(?, ?)').all(2, 3)) .to.deep.equal([{ a: 2, b: 3 }, { a: 4, b: 9 }]); expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ?').all(2, 3)) .to.deep.equal([{ a: 2, b: 3 }, { a: 4, b: 9 }]); expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?)')) .to.throw(Database.SqliteError); expect(() => this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ? AND "$3" = ?')) .to.throw(Database.SqliteError); }); it('should accept explicit parameters for the virtual table', function () { this.db.table('vtab', { columns: ['a', 'b'], parameters: ['x', 'y', 'z'], *rows(p1, p2, p3, p4) { yield [arguments[0], arguments[1] + arguments[2]]; yield [arguments[0] * 2, (arguments[1] + arguments[2]) * 3]; }, }); expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').all(2, 3, 4)) .to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]); expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').all(2, 3, 4)) .to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]); expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?)')) .to.throw(Database.SqliteError); expect(() => this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ? AND "$3" = ?')) .to.throw(Database.SqliteError); }); it('should accept a large number of parameters for the virtual table', function () { const args = ['foo', 'bar', 1, -2, Buffer.from('hello'), 5, -10, 'baz', 99.9, -0.5]; this.db.table('vtab', { columns: ['x'], *rows(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) { yield [p10]; yield [p9]; yield [p8]; yield [p7]; yield [p6]; yield [p5]; yield [p4]; yield [p3]; yield [p2]; yield [p1]; }, }); expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').pluck().all(args)) .to.deep.equal(args.slice().reverse()); expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?)').pluck().all(args.slice(0, -1))) .to.deep.equal([null].concat(args.slice(0, -1).reverse())); expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')) .to.throw(Database.SqliteError); }); it('should correctly handle arguments even when used out of order', function () { const calls = []; this.db.table('vtab', { columns: ['x', 'y'], *rows(x, y) { calls.push([...arguments]); yield { x, y }; }, }); expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ?').get(10, 5)) .to.deep.equal({ x: 10, y: 5 }); expect(calls.splice(0)).to.deep.equal([[10, 5]]); expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$1" = ?').get(5, 10)) .to.deep.equal({ x: 10, y: 5 }); expect(calls.splice(0)).to.deep.equal([[10, 5]]); expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(5, 5, 10)) .to.deep.equal({ x: 10, y: 5 }); expect(calls.splice(0)).to.deep.equal([[10, 5]]); expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(5, 9, 10)) .to.be.undefined; expect(calls.splice(0)).to.deep.equal([]); expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(9, 5, 10)) .to.be.undefined; expect(calls.splice(0)).to.deep.equal([]); }); it('should correctly handle arguments that are constrained to other arguments', function () { const calls = []; this.db.table('vtab', { columns: ['x', 'y'], *rows(x, y) { calls.push([...arguments]); yield { x, y }; }, }); expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = "$1"').get(10)) .to.deep.equal({ x: 10, y: 10 }); expect(calls.splice(0)).to.deep.equal([[10, 10]]); expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = "$1" AND "$1" = ?').get(10)) .to.deep.equal({ x: 10, y: 10 }); expect(calls.splice(0)).to.deep.equal([[10, 10]]); expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = "$1" AND "$1" = ?').get(10, 10)) .to.deep.equal({ x: 10, y: 10 }); expect(calls.splice(0)).to.deep.equal([[10, 10]]); expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = "$1" AND "$1" = ?').get(5, 10)) .to.be.undefined; expect(calls.splice(0)).to.deep.equal([]); expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = "$1" AND "$2" = ? AND "$1" = ?').get(5, 10)) .to.be.undefined; expect(calls.splice(0)).to.deep.equal([]); }); 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.table('a', { columns: ['x'], rows: function* () {} })).to.throw(TypeError); } expect(ranOnce).to.be.true; this.db.table('b', { columns: ['x'], rows: function* () {} }); }); it('should cause the database to become busy when querying the virtual table', function () { let checkCount = 0; const expectBusy = function* () { for (let i = 0; i < 3; ++i) { expect(() => this.db.exec('SELECT * FROM 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.table('y', { columns: ['x'], rows: function* () {} })).to.throw(TypeError); checkCount += 1; yield [i]; } }; this.db.table('a', { columns: ['x'], rows: function* () {} }); this.db.table('b', { columns: ['x'], rows: expectBusy }); expect(this.db.prepare('SELECT * FROM b').pluck().all()).to.deep.equal([0, 1, 2]); expect(checkCount).to.equal(3); this.db.exec('SELECT * FROM a'); this.db.prepare('SELECT 555'); this.db.pragma('cache_size'); this.db.function('xx', () => {}); this.db.table('yy', { columns: ['x'], rows: function* () {} }) }); it('should cause the virtual table to throw when yielding an invalid value', function () { this.db.table('a', { columns: ['x'], *rows() { yield [42]; } }); this.db.table('b', { columns: ['x'], *rows() { yield 42; } }); this.db.table('c', { columns: ['x'], *rows() { yield; } }); this.db.table('d', { columns: ['x'], *rows() { yield null; } }); expect(this.db.prepare('SELECT * FROM a').get()).to.deep.equal({ x: 42 }); expect(() => this.db.prepare('SELECT * FROM b').get()).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM c').get()).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM d').get()).to.throw(TypeError); }); it('should allow arrays to be yielded as rows', function () { const rows = [ { a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') }, { a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') }, ]; this.db.table('vtab', { columns: ['a', 'b', 'c', 'd', 'e'], *rows() { for (const obj of rows) { yield Object.values(obj); } }, }); expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows); }); it('should allow objects to be yielded as rows', function () { const rows = [ { a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') }, { a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') }, { e: Buffer.from('hello'), d: 'world', c: 0.1, b: 10, a: null }, { d: 'old friend', c: -0.1, e: Buffer.from('goodbye'), a: null, b: -10 }, ]; this.db.table('vtab', { columns: ['a', 'b', 'c', 'd', 'e'], *rows() { for (const obj of rows) { yield obj; } }, }); expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows); }); it('should throw an exception if an invalid array is yielded', function () { const tests = [ [1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6], [1, 2, 3, 4], [], [1, 2, 3, 4, new Number(5)], [1, 2, 3, 4, [5]], [1, 2, 3, 4, new Date()], ]; this.db.table('vtab', { columns: ['a', 'b', 'c', 'd', 'e'], *rows(n) { yield tests[n]; }, }); expect(this.db.prepare('SELECT * FROM vtab(?)').raw().all(0)).to.deep.equal([tests[0]]); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(1)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(2)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(3)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(4)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(5)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(6)).to.throw(TypeError); }); it('should throw an exception if an invalid object is yielded', function () { const tests = [ { a: 1, b: 2, c: 3, d: 4, e: 5 }, { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }, { a: 1, b: 2, c: 3, d: 4 }, {}, { a: 1, b: 2, c: 3, d: 4, e: new Number(5) }, { a: 1, b: 2, c: 3, d: 4, e: [5] }, { a: 1, b: 2, c: 3, d: 4, e: new Date() }, { a: 1, b: 2, c: 3, d: 4, f: 5 }, ]; this.db.table('vtab', { columns: ['a', 'b', 'c', 'd', 'e'], *rows(n) { yield tests[n]; }, }); expect(this.db.prepare('SELECT * FROM vtab(?)').all(0)).to.deep.equal([tests[0]]); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(1)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(2)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(3)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(4)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(5)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(6)).to.throw(TypeError); expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(7)).to.throw(TypeError); }); it('should automatically assign rowids without affecting yielded objects', function () { let rows = [{ x: 5 }, { x: 10 }]; this.db.table('a', { columns: ['x'], *rows() { yield* rows; }, }); expect(this.db.prepare('SELECT rowid, * FROM a').all()) .to.deep.equal([{ rowid: 1, x: 5 }, { rowid: 2, x: 10 }]); expect(rows).to.deep.equal([{ x: 5 }, { x: 10 }]); rows = [{ rowid: 5 }, { rowid: 10 }]; this.db.table('b', { columns: ['rowid'], *rows() { yield* rows; }, }); expect(this.db.prepare('SELECT oid AS oid, * FROM b').all()) .to.deep.equal([{ oid: 1, rowid: 5 }, { oid: 2, rowid: 10 }]); expect(rows).to.deep.equal([{ rowid: 5 }, { rowid: 10 }]); }); it('should be driven by stmt.iterate() one row at a time', function () { let state = 0; this.db.table('vtab', { columns: ['x'], *rows() { state += 1; yield ['foo']; state += 1; yield ['bar']; state += 1; yield ['baz']; state += 1; yield ['qux']; state += 1; }, }); const values = []; for (const value of this.db.prepare('SELECT * FROM vtab').pluck().iterate()) { values.push(value); if (value === 'baz') break; } expect(values).to.deep.equal(['foo', 'bar', 'baz']); expect(state).to.equal(3); }); it('should throw an exception if preparing a statement that uses an unsupported operator on a parameter', function () { this.db.table('vtab', { columns: ['a', 'b'], parameters: ['x', 'y', 'z'], *rows(x, y, z) { yield [x, y + z]; yield [x * 2, (y + z) * 3]; }, }); expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').all(2, 3, 4)) .to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]); expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').all(2, 3, 4)) .to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]); expect(() => this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z > ?')) .to.throw(Database.SqliteError); expect(() => this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y < ? AND z = ?')) .to.throw(Database.SqliteError); expect(() => this.db.prepare('SELECT * FROM vtab WHERE x IS ? AND y = ? AND z = ?')) .to.throw(Database.SqliteError); }); it('should properly escape column and parameter names', function () { this.db.table('vtab', { columns: ['foo);'], parameters: ['x"); SELECT "y', 'y'], *rows(x, y) { yield [x]; yield [y]; yield [x + y]; }, }); expect(this.db.prepare('SELECT "foo);" FROM vtab WHERE "x""); SELECT ""y" = ? AND y = ?').all(5, 10)) .to.deep.equal([{ 'foo);': 5 }, { 'foo);': 10 }, { 'foo);': 15 }]); }); it('should not allow CREATE VIRTUAL TABLE statements by default', function () { this.db.table('mod', { columns: ['x'], *rows() {}, }); expect(() => this.db.exec('CREATE VIRTUAL TABLE a USING mod')).to.throw(Database.SqliteError); expect(() => this.db.exec('CREATE VIRTUAL TABLE b USING mod()')).to.throw(Database.SqliteError); expect(() => this.db.exec('CREATE VIRTUAL TABLE c USING mod(foo)')).to.throw(Database.SqliteError); }); it('should support CREATE VIRTUAL TABLE statements by accepting a factory function', function () { let table = ''; this.db.table('mod', function (...args) { expect(this).to.deep.equal({ module: 'mod', database: 'main', table }); return { columns: ['x'], *rows() { yield* args.map(x => [x]); }, }; }); expect(() => this.db.prepare('SELECT * FROM mod')).to.throw(Database.SqliteError); table = 'foo'; this.db.exec(`CREATE VIRTUAL TABLE ${table} USING mod(hello world, how are you?)`); table = 'bar'; this.db.exec(`CREATE VIRTUAL TABLE ${table} USING mod(1, 2, 3)`); expect(this.db.prepare('SELECT x FROM foo').pluck().all()).to.deep.equal(['hello world', 'how are you?']); expect(this.db.prepare('SELECT x FROM bar').pluck().all()).to.deep.equal(['1', '2', '3']); expect(() => this.db.prepare('SELECT * FROM mod')).to.throw(Database.SqliteError); }); it('should correctly handle omitted arguments in any order', function () { this.db.table('vtab', { columns: ['value'], parameters: ['x', 'y', 'z'], *rows(x = 100, y = 10, z = 1) { expect(arguments.length).to.equal(3); yield [x + y + z]; }, }); expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').pluck().get(2.2, 3.3, 4.4)).to.equal(9.9); expect(this.db.prepare('SELECT * FROM vtab(?, ?)').pluck().get(2.2, 3.3)).to.equal(6.5); expect(this.db.prepare('SELECT * FROM vtab(?)').pluck().get(2.2)).to.equal(13.2); expect(this.db.prepare('SELECT * FROM vtab').pluck().get()).to.equal(111); expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').pluck().get(2.2, 3.3, 4.4)).to.equal(9.9); expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ?').pluck().get(2.2, 3.3)).to.equal(6.5); expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND z = ?').pluck().get(2.2, 3.3)).to.equal(15.5); expect(this.db.prepare('SELECT * FROM vtab WHERE y = ? AND z = ?').pluck().get(2.2, 3.3)).to.equal(105.5); expect(this.db.prepare('SELECT * FROM vtab WHERE x = ?').pluck().get(2.2)).to.equal(13.2); expect(this.db.prepare('SELECT * FROM vtab WHERE y = ?').pluck().get(2.2)).to.equal(103.2); expect(this.db.prepare('SELECT * FROM vtab WHERE z = ?').pluck().get(2.2)).to.equal(112.2); }); it('should not call the generator function if any arguments are NULL', function () { let calls = 0; this.db.table('vtab', { columns: ['val'], parameters: ['x', 'y', 'z'], *rows(x = 0, y = 0, z = 0) { calls += 1; yield [x + y + z]; }, }); expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, 10, 100)).to.deep.equal([111]); expect(this.db.prepare('SELECT val FROM vtab(?, ?)').pluck().all(1, 10)).to.deep.equal([11]); expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, 10, null)).to.deep.equal([]); expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, null, 100)).to.deep.equal([]); expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(null, 10, 100)).to.deep.equal([]); expect(this.db.prepare('SELECT val FROM vtab(?, ?)').pluck().all(1, null)).to.deep.equal([]); expect(calls).to.equal(2); }); it('should close a statement iterator that caused a virtual table to throw', function () { this.db.prepare('CREATE TABLE iterable (x 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.table('vtab', { columns: ['value'], parameters: ['x'], *rows(x) { if (++i >= 5) throw err; yield [x]; }, }); const iterator = this.db.prepare('SELECT value FROM vtab JOIN iterable USING (x)').pluck().iterate(); let total = 0; expect(() => { for (const value of iterator) { total += value; expect(() => this.db.exec('SELECT value FROM vtab JOIN iterable USING (x) LIMIT 4')).to.throw(TypeError); } }).to.throw(err); expect(total).to.equal(1 + 2 + 4 + 8); expect(iterator.next()).to.deep.equal({ value: undefined, done: true }); expect(total).to.equal(1 + 2 + 4 + 8); i = 0; this.db.exec('SELECT value FROM vtab JOIN iterable USING (x) LIMIT 4'); expect(i).to.equal(4); }); it('should not be able to affect bound buffers mid-query', function () { const input = Buffer.alloc(1024 * 8).fill(0xbb); let called = false; this.db.table('vtab', { columns: ['x'], *rows(arg) { called = true; input[0] = 2; arg[0] = 2; yield [123]; }, }); const [output, arg, num] = this.db.prepare('SELECT :input, "$1", x FROM vtab(:input)').raw().get({ input }); expect(called).to.be.true; expect(output.equals(Buffer.alloc(1024 * 8).fill(0xbb))).to.be.true; expect(arg.equals(Buffer.alloc(1024 * 8).fill(0xbb))).to.be.true; expect(num).to.equal(123); }); 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 table to throw an exception'); }; specify('thrown in the factory function', function () { exceptions.forEach((exception, index) => { const calls = []; this.db.table(`mod${index}`, () => { calls.push('a'); throw exception; calls.push('b'); return { columns: ['x'], *rows() { calls.push('c'); yield [42]; calls.push('d'); }, }; }); expect(calls.splice(0)).to.deep.equal([]); expectError(exception, () => this.db.exec(`CREATE VIRTUAL TABLE vtab${index} USING mod${index}()`)); expect(calls.splice(0)).to.deep.equal(['a']); expect(() => this.db.prepare(`SELECT * FROM vtab${index}`)).to.throw(Database.SqliteError); expect(calls.splice(0)).to.deep.equal([]); }); }); specify('thrown in the rows() function', function () { exceptions.forEach((exception, index) => { const calls = []; this.db.table(`mod${index}`, () => { calls.push('a'); return { columns: ['x'], *rows() { calls.push('b'); yield [42]; calls.push('c'); throw exception; calls.push('d'); }, }; }); expect(calls.splice(0)).to.deep.equal([]); this.db.exec(`CREATE VIRTUAL TABLE vtab${index} USING mod${index}()`); expect(calls.splice(0)).to.deep.equal(['a']); expect(this.db.prepare(`SELECT * FROM vtab${index}`).pluck().get()).to.equal(42); expect(calls.splice(0)).to.deep.equal(['b']); expectError(exception, () => this.db.prepare(`SELECT * FROM vtab${index}`).pluck().all()); expect(calls.splice(0)).to.deep.equal(['b', 'c']); }); }); specify('thrown due to yielding an invalid value', function () { const calls = []; this.db.table('mod', () => { calls.push('a'); return { columns: ['x'], *rows() { calls.push('b'); yield [42]; calls.push('c'); yield [new Number(42)]; calls.push('d'); }, }; }); expect(calls.splice(0)).to.deep.equal([]); this.db.exec('CREATE VIRTUAL TABLE vtab USING mod()'); expect(calls.splice(0)).to.deep.equal(['a']); expect(this.db.prepare('SELECT * FROM vtab').pluck().get()).to.equal(42); expect(calls.splice(0)).to.deep.equal(['b']); expect(() => this.db.prepare('SELECT * FROM vtab').pluck().all()).to.throw(TypeError); expect(calls.splice(0)).to.deep.equal(['b', 'c']); }); }); describe('should not affect external environment', function () { specify('busy state', function () { this.db.table('vtab', { columns: ['x'], *rows(arg) { expect(() => this.db.exec('SELECT 555')).to.throw(TypeError); yield [arg * 2]; }, }); let ranOnce = false; for (const x of this.db.prepare('SELECT * FROM vtab(555)').pluck().iterate()) { ranOnce = true; expect(x).to.equal(1110); expect(() => this.db.exec('SELECT 555')).to.throw(TypeError); } expect(ranOnce).to.be.true; this.db.exec('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.table('vtab', { columns: ['x'], *rows() { throw err; }, }); expect(() => this.db.prepare('SELECT * FROM vtab').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'); }); }); it('should correctly handle limit and offset clause', function () { let lastValue; this.db.table('vtab', { columns: ['x'], *rows() { lastValue = 1; yield { x: lastValue }; lastValue = 2; yield { x: lastValue }; lastValue = 3; yield { x: lastValue }; lastValue = null; }, }); expect(this.db.prepare('SELECT * FROM vtab LIMIT 1').all()) .to.deep.equal([{ x: 1 }]); expect(lastValue).to.equal(1); expect(this.db.prepare('SELECT * FROM vtab LIMIT 1 OFFSET 2').all()) .to.deep.equal([{ x: 3 }]); expect(lastValue).to.equal(3); expect(this.db.prepare('SELECT * FROM vtab LIMIT 100 OFFSET 1').all()) .to.deep.equal([{ x: 2 }, { x: 3 }]); expect(lastValue).to.be.null; }); });