mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
The "non-integer callback length" suite in test-sqlite-options-getter-reentry.js checks that function() and aggregate() reject a callback whose length property is not an integer. That is plain argument validation: no property getter runs, and nothing changes mid-call, so the tests do not belong in a file about option getters re-entering the database. Move them into the "input validation" suites that already cover the other argument type checks for each method. Drop "a normal function length is still accepted", which duplicates "uses function.length when false" in test-sqlite-custom-functions.js. The two tests that put the getter on length itself stay where they are, since closing the database from that getter is what they exercise. Refs: https://github.com/nodejs/node/pull/65595 Signed-off-by: Trevor Burnham <trevorburnham@gmail.com> PR-URL: https://github.com/nodejs/node/pull/65769 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
473 lines
13 KiB
JavaScript
473 lines
13 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:');
|
|
|
|
// The length property is configurable, so any type can reach the
|
|
// conversion that derives the aggregate's arity from it.
|
|
const lengths = ['abc', {}, [], null, undefined, NaN, 1.5,
|
|
Symbol.iterator, 10n, true, 2 ** 40];
|
|
|
|
function withLength(fn, length) {
|
|
Object.defineProperty(fn, 'length', {
|
|
configurable: true,
|
|
value: length,
|
|
});
|
|
return fn;
|
|
}
|
|
|
|
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/,
|
|
});
|
|
});
|
|
|
|
test('throws if options.step.length is not an integer', (t) => {
|
|
for (const length of lengths) {
|
|
t.assert.throws(() => {
|
|
db.aggregate('sum', {
|
|
start: 0,
|
|
step: withLength((acc, value) => acc + value, length),
|
|
result: (total) => total
|
|
});
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "options\.step\.length" property must be an integer/,
|
|
}, `length=${String(length)}`);
|
|
}
|
|
});
|
|
|
|
test('throws if options.inverse.length is not an integer', (t) => {
|
|
for (const length of lengths) {
|
|
t.assert.throws(() => {
|
|
db.aggregate('sum', {
|
|
start: 0,
|
|
step: (acc, value) => acc + value,
|
|
inverse: withLength((acc, value) => acc - value, length),
|
|
result: (total) => total
|
|
});
|
|
}, {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: /The "options\.inverse\.length" property must be an integer/,
|
|
}, `length=${String(length)}`);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
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'
|
|
});
|
|
});
|