| import { skipIfSQLiteMissing } from '../common/index.mjs'; |
| import { describe, test } from 'node:test'; |
| skipIfSQLiteMissing(); |
| const { DatabaseSync } = await import('node:sqlite'); |
| |
| describe('DatabaseSync.prototype.aggregate()', () => { |
| describe('input validation', () => { |
| const db = new DatabaseSync(':memory:'); |
| |
| test('throws if options.start is not provided', (t) => { |
| t.assert.throws(() => { |
| db.aggregate('sum', { |
| result: (total) => total |
| }); |
| }, { |
| code: 'ERR_INVALID_ARG_TYPE', |
| message: 'The "options.start" argument must be a function or a primitive value.' |
| }); |
| }); |
| |
| test('throws if options.step is not a function', (t) => { |
| t.assert.throws(() => { |
| db.aggregate('sum', { |
| start: 0, |
| result: (total) => total |
| }); |
| }, { |
| code: 'ERR_INVALID_ARG_TYPE', |
| message: 'The "options.step" argument must be a function.' |
| }); |
| }); |
| |
| test('throws if options.useBigIntArguments is not a boolean', (t) => { |
| t.assert.throws(() => { |
| db.aggregate('sum', { |
| start: 0, |
| step: () => null, |
| useBigIntArguments: '' |
| }); |
| }, { |
| code: 'ERR_INVALID_ARG_TYPE', |
| message: /The "options\.useBigIntArguments" argument must be a boolean/, |
| }); |
| }); |
| |
| test('throws if options.varargs is not a boolean', (t) => { |
| t.assert.throws(() => { |
| db.aggregate('sum', { |
| start: 0, |
| step: () => null, |
| varargs: '' |
| }); |
| }, { |
| code: 'ERR_INVALID_ARG_TYPE', |
| message: /The "options\.varargs" argument must be a boolean/, |
| }); |
| }); |
| |
| test('throws if options.directOnly is not a boolean', (t) => { |
| t.assert.throws(() => { |
| db.aggregate('sum', { |
| start: 0, |
| step: () => null, |
| directOnly: '' |
| }); |
| }, { |
| code: 'ERR_INVALID_ARG_TYPE', |
| message: /The "options\.directOnly" argument must be a boolean/, |
| }); |
| }); |
| |
| test('throws if options.inverse is not a function', (t) => { |
| t.assert.throws(() => { |
| db.aggregate('sum', { |
| start: 0, |
| step: (acc, value) => acc + value, |
| inverse: 10 |
| }); |
| }, { |
| code: 'ERR_INVALID_ARG_TYPE', |
| message: /The "options\.inverse" argument must be a function/, |
| }); |
| }); |
| }); |
| }); |
| |
| describe('varargs', () => { |
| test('supports variable number of arguments when true', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.exec('CREATE TABLE data (value INTEGER)'); |
| db.exec('INSERT INTO data VALUES (1), (2), (3)'); |
| db.aggregate('sum_int', { |
| start: 0, |
| step: (_acc, _value, var1, var2, var3) => { |
| return var1 + var2 + var3; |
| }, |
| varargs: true, |
| }); |
| |
| const result = db.prepare('SELECT sum_int(value, 1, 2, 3) as total FROM data').get(); |
| |
| t.assert.deepStrictEqual(result, { __proto__: null, total: 6 }); |
| }); |
| |
| test('uses the max between step.length and inverse.length when false', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.exec(` |
| CREATE TABLE t3(x, y); |
| INSERT INTO t3 VALUES ('a', 1), |
| ('b', 2), |
| ('c', 3); |
| `); |
| |
| db.aggregate('sumint', { |
| start: 0, |
| step: (acc, var1) => { |
| return var1 + acc; |
| }, |
| inverse: (acc, var1, var2) => { |
| return acc - var1 - var2; |
| }, |
| varargs: false, |
| }); |
| |
| const result = db.prepare(` |
| SELECT x, sumint(y, 10) OVER ( |
| ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING |
| ) AS sum_y |
| FROM t3 ORDER BY x; |
| `).all(); |
| |
| t.assert.deepStrictEqual(result, [ |
| { __proto__: null, x: 'a', sum_y: 3 }, |
| { __proto__: null, x: 'b', sum_y: 6 }, |
| { __proto__: null, x: 'c', sum_y: -5 }, |
| ]); |
| |
| t.assert.throws(() => { |
| db.prepare(` |
| SELECT x, sumint(y) OVER ( |
| ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING |
| ) AS sum_y |
| FROM t3 ORDER BY x; |
| `); |
| }, { |
| code: 'ERR_SQLITE_ERROR', |
| message: 'wrong number of arguments to function sumint()' |
| }); |
| }); |
| |
| test('throws if an incorrect number of arguments is provided when false', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.aggregate('sum_int', { |
| start: 0, |
| step: (_acc, var1, var2, var3) => { |
| return var1 + var2 + var3; |
| }, |
| varargs: false, |
| }); |
| |
| t.assert.throws(() => { |
| db.prepare('SELECT sum_int(1, 2, 3, 4)').get(); |
| }, { |
| code: 'ERR_SQLITE_ERROR', |
| message: 'wrong number of arguments to function sum_int()' |
| }); |
| }); |
| }); |
| |
| describe('directOnly', () => { |
| test('is false by default', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.aggregate('func', { |
| start: 0, |
| step: (acc, value) => acc + value, |
| inverse: (acc, value) => acc - value, |
| }); |
| db.exec(` |
| CREATE TABLE t3(x, y); |
| INSERT INTO t3 VALUES ('a', 4), |
| ('b', 5), |
| ('c', 3); |
| `); |
| |
| db.exec(` |
| CREATE TRIGGER test_trigger |
| AFTER INSERT ON t3 |
| BEGIN |
| SELECT func(1) OVER (); |
| END; |
| `); |
| |
| // TRIGGER will work fine with the window function |
| db.exec('INSERT INTO t3 VALUES(\'d\', 6)'); |
| }); |
| |
| test('set SQLITE_DIRECT_ONLY flag when true', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.aggregate('func', { |
| start: 0, |
| step: (acc, value) => acc + value, |
| inverse: (acc, value) => acc - value, |
| directOnly: true, |
| }); |
| db.exec(` |
| CREATE TABLE t3(x, y); |
| INSERT INTO t3 VALUES ('a', 4), |
| ('b', 5), |
| ('c', 3); |
| `); |
| |
| db.exec(` |
| CREATE TRIGGER test_trigger |
| AFTER INSERT ON t3 |
| BEGIN |
| SELECT func(1) OVER (); |
| END; |
| `); |
| |
| t.assert.throws(() => { |
| db.exec('INSERT INTO t3 VALUES(\'d\', 6)'); |
| }, { |
| code: 'ERR_SQLITE_ERROR', |
| message: /unsafe use of func\(\)/ |
| }); |
| }); |
| }); |
| |
| describe('start', () => { |
| test('start option as a value', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.exec('CREATE TABLE data (value INTEGER)'); |
| db.exec('INSERT INTO data VALUES (1), (2), (3)'); |
| db.aggregate('sum_int', { |
| start: 0, |
| step: (acc, value) => acc + value, |
| }); |
| |
| const result = db.prepare('SELECT sum_int(value) as total FROM data').get(); |
| |
| t.assert.deepStrictEqual(result, { __proto__: null, total: 6 }); |
| }); |
| |
| test('start option as a function', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.exec('CREATE TABLE data (value INTEGER)'); |
| db.exec('INSERT INTO data VALUES (1), (2), (3)'); |
| db.aggregate('sum_int', { |
| start: () => 0, |
| step: (acc, value) => acc + value, |
| }); |
| |
| const result = db.prepare('SELECT sum_int(value) as total FROM data').get(); |
| |
| t.assert.deepStrictEqual(result, { __proto__: null, total: 6 }); |
| }); |
| |
| test('start option can hold any js value', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.exec('CREATE TABLE data (value INTEGER)'); |
| db.exec('INSERT INTO data VALUES (1), (2), (3)'); |
| db.aggregate('sum_int', { |
| start: () => [], |
| step: (acc, value) => { |
| return [...acc, value]; |
| }, |
| result: (acc) => acc.join(', '), |
| }); |
| |
| const result = db.prepare('SELECT sum_int(value) as total FROM data').get(); |
| |
| t.assert.deepStrictEqual(result, { __proto__: null, total: '1, 2, 3' }); |
| }); |
| |
| test('throws if start throws an error', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.exec('CREATE TABLE data (value INTEGER)'); |
| db.exec('INSERT INTO data VALUES (1), (2), (3)'); |
| db.aggregate('agg', { |
| start: () => { |
| throw new Error('start error'); |
| }, |
| step: () => null, |
| }); |
| |
| t.assert.throws(() => { |
| db.prepare('SELECT agg()').get(); |
| }, { |
| message: 'start error' |
| }); |
| }); |
| }); |
| |
| describe('step', () => { |
| test('throws if step throws an error', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.exec('CREATE TABLE data (value INTEGER)'); |
| db.exec('INSERT INTO data VALUES (1), (2), (3)'); |
| db.aggregate('agg', { |
| start: 0, |
| step: () => { |
| throw new Error('step error'); |
| }, |
| }); |
| |
| t.assert.throws(() => { |
| db.prepare('SELECT agg()').get(); |
| }, { |
| message: 'step error' |
| }); |
| }); |
| }); |
| |
| describe('result', () => { |
| test('throws if result throws an error', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.exec('CREATE TABLE data (value INTEGER)'); |
| db.exec('INSERT INTO data VALUES (1), (2), (3)'); |
| db.aggregate('sum_int', { |
| start: 0, |
| step: (acc, value) => { |
| return acc + value; |
| }, |
| result: () => { |
| throw new Error('result error'); |
| }, |
| }); |
| t.assert.throws(() => { |
| db.prepare('SELECT sum_int(value) as result FROM data').get(); |
| }, { |
| message: 'result error' |
| }); |
| }); |
| |
| test('executes once when options.inverse is not present', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| const mockFn = t.mock.fn(() => 'overridden'); |
| db.exec('CREATE TABLE data (value INTEGER)'); |
| db.exec('INSERT INTO data VALUES (1), (2), (3)'); |
| db.aggregate('sum_int', { |
| start: 0, |
| step: (acc, value) => { |
| return acc + value; |
| }, |
| result: mockFn |
| }); |
| |
| const result = db.prepare('SELECT sum_int(value) as result FROM data').get(); |
| |
| t.assert.deepStrictEqual(result, { __proto__: null, result: 'overridden' }); |
| t.assert.strictEqual(mockFn.mock.calls.length, 1); |
| t.assert.deepStrictEqual(mockFn.mock.calls[0].arguments, [6]); |
| }); |
| |
| test('executes once per row when options.inverse is present', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| const mockFn = t.mock.fn((acc) => acc); |
| db.exec(` |
| CREATE TABLE t3(x, y); |
| INSERT INTO t3 VALUES ('a', 4), |
| ('b', 5), |
| ('c', 3); |
| `); |
| db.aggregate('sumint', { |
| start: 0, |
| step: (acc, value) => { |
| return acc + value; |
| }, |
| inverse: (acc, value) => { |
| return acc - value; |
| }, |
| result: mockFn |
| }); |
| |
| db.prepare(` |
| SELECT x, sumint(y) OVER ( |
| ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING |
| ) AS sum_y |
| FROM t3 ORDER BY x; |
| `).all(); |
| |
| t.assert.strictEqual(mockFn.mock.calls.length, 3); |
| t.assert.deepStrictEqual(mockFn.mock.calls[0].arguments, [9]); |
| t.assert.deepStrictEqual(mockFn.mock.calls[1].arguments, [12]); |
| t.assert.deepStrictEqual(mockFn.mock.calls[2].arguments, [8]); |
| }); |
| }); |
| |
| test('throws an error when trying to use as windown function but didn\'t provide options.inverse', (t) => { |
| const db = new DatabaseSync(':memory:'); |
| t.after(() => db.close()); |
| db.exec(` |
| CREATE TABLE t3(x, y); |
| INSERT INTO t3 VALUES ('a', 4), |
| ('b', 5), |
| ('c', 3); |
| `); |
| |
| db.aggregate('sumint', { |
| start: 0, |
| step: (total, nextValue) => total + nextValue, |
| }); |
| |
| t.assert.throws(() => { |
| db.prepare(` |
| SELECT x, sumint(y) OVER ( |
| ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING |
| ) AS sum_y |
| FROM t3 ORDER BY x; |
| `); |
| }, { |
| code: 'ERR_SQLITE_ERROR', |
| message: 'sumint() may not be used as a window function' |
| }); |
| }); |