nodejs/test/parallel/test-sqlite-aggregate-function.mjs
Michael Dawson 535c2f7562 sqlite: add build option to build without sqlite
Signed-off-by: Michael Dawson <midawson@redhat.com>
PR-URL: https://github.com/nodejs/node/pull/58122
Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
2025-05-07 17:20:57 -04:00

395 lines
11 KiB
JavaScript

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/,
});
});
});
});
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('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'
});
});