Files
node/test/parallel/test-sqlite-custom-functions.js
T
Trevor Burnham 523ac2c43d test: move sqlite length validation out of the reentry test
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>
2026-09-08 17:33:51 +00:00

437 lines
14 KiB
JavaScript

'use strict';
const { skipIfSQLiteMissing, mustCall } = require('../common');
skipIfSQLiteMissing();
const assert = require('node:assert');
const { DatabaseSync } = require('node:sqlite');
const { suite, test } = require('node:test');
suite('DatabaseSync.prototype.function()', () => {
suite('input validation', () => {
const db = new DatabaseSync(':memory:');
test('throws if name is not a string', () => {
assert.throws(() => {
db.function();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "name" argument must be a string/,
});
});
test('throws if function is not a function', () => {
assert.throws(() => {
db.function('foo');
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "function" argument must be a function/,
});
});
test('throws if options is not an object', () => {
assert.throws(() => {
db.function('foo', null, () => {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options" argument must be an object/,
});
});
test('throws if options.useBigIntArguments is not a boolean', () => {
assert.throws(() => {
db.function('foo', { useBigIntArguments: null }, () => {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.useBigIntArguments" argument must be a boolean/,
});
});
test('throws if options.varargs is not a boolean', () => {
assert.throws(() => {
db.function('foo', { varargs: null }, () => {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.varargs" argument must be a boolean/,
});
});
test('throws if options.deterministic is not a boolean', () => {
assert.throws(() => {
db.function('foo', { deterministic: null }, () => {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.deterministic" argument must be a boolean/,
});
});
test('throws if options.directOnly is not a boolean', () => {
assert.throws(() => {
db.function('foo', { directOnly: null }, () => {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.directOnly" argument must be a boolean/,
});
});
test('throws if function.length is not an integer', () => {
// The length property is configurable, so any type can reach the
// conversion that derives the function's arity from it.
const lengths = ['abc', {}, [], null, undefined, NaN, 1.5,
Symbol.iterator, 10n, true, 2 ** 40];
for (const length of lengths) {
const fn = () => 1;
Object.defineProperty(fn, 'length', {
configurable: true,
value: length,
});
assert.throws(() => {
db.function('foo', fn);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "function\.length" property must be an integer/,
}, `length=${String(length)}`);
}
});
});
suite('useBigIntArguments', () => {
test('converts arguments to BigInts when true', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', { useBigIntArguments: true }, (arg) => {
value = arg;
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(5) AS custom').get();
assert.strictEqual(value, 5n);
});
test('uses number primitives when false', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', { useBigIntArguments: false }, (arg) => {
value = arg;
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(5) AS custom').get();
assert.strictEqual(value, 5);
});
test('defaults to false', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', (arg) => {
value = arg;
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(5) AS custom').get();
assert.strictEqual(value, 5);
});
test('throws if value cannot fit in a number', () => {
const db = new DatabaseSync(':memory:');
const value = Number.MAX_SAFE_INTEGER + 1;
db.function('custom', (arg) => {});
assert.throws(() => {
db.prepare(`SELECT custom(${value}) AS custom`).get();
}, {
code: 'ERR_OUT_OF_RANGE',
message: /Value is too large to be represented as a JavaScript number: 9007199254740992/,
});
});
});
suite('varargs', () => {
test('supports variable number of arguments when true', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', { varargs: true }, (...args) => {
value = args;
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(5, 4, 3, 2, 1) AS custom').get();
assert.deepStrictEqual(value, [5, 4, 3, 2, 1]);
});
test('uses function.length when false', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', { varargs: false }, (a, b, c) => {
value = [a, b, c];
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(1, 2, 3) AS custom').get();
assert.deepStrictEqual(value, [1, 2, 3]);
});
test('defaults to false', () => {
const db = new DatabaseSync(':memory:');
let value;
const r = db.function('custom', (a, b, c) => {
value = [a, b, c];
});
assert.strictEqual(r, undefined);
db.prepare('SELECT custom(7, 8, 9) AS custom').get();
assert.deepStrictEqual(value, [7, 8, 9]);
});
test('throws if an incorrect number of arguments is provided', () => {
const db = new DatabaseSync(':memory:');
db.function('custom', (a, b, c, d) => {});
assert.throws(() => {
db.prepare('SELECT custom(1, 2, 3) AS custom').get();
}, {
code: 'ERR_SQLITE_ERROR',
message: /wrong number of arguments to function custom\(\)/,
});
});
});
suite('deterministic', () => {
test('creates a deterministic function when true', () => {
const db = new DatabaseSync(':memory:');
db.function('isDeterministic', { deterministic: true }, () => {
return 42;
});
const r = db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (isDeterministic()) VIRTUAL
)
`);
assert.strictEqual(r, undefined);
});
test('creates a non-deterministic function when false', () => {
const db = new DatabaseSync(':memory:');
db.function('isNonDeterministic', { deterministic: false }, () => {
return 42;
});
assert.throws(() => {
db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (isNonDeterministic()) VIRTUAL
)
`);
}, {
code: 'ERR_SQLITE_ERROR',
message: /non-deterministic functions prohibited in generated columns/,
});
});
test('deterministic defaults to false', () => {
const db = new DatabaseSync(':memory:');
db.function('isNonDeterministic', () => {
return 42;
});
assert.throws(() => {
db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (isNonDeterministic()) VIRTUAL
)
`);
}, {
code: 'ERR_SQLITE_ERROR',
message: /non-deterministic functions prohibited in generated columns/,
});
});
});
suite('directOnly', () => {
test('sets SQLite direct only flag when true', () => {
const db = new DatabaseSync(':memory:');
db.function('fn', { deterministic: true, directOnly: true }, () => {
return 42;
});
assert.throws(() => {
db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (fn()) VIRTUAL
)
`);
}, {
code: 'ERR_SQLITE_ERROR',
message: /unsafe use of fn\(\)/
});
});
test('does not set SQLite direct only flag when false', () => {
const db = new DatabaseSync(':memory:');
db.function('fn', { deterministic: true, directOnly: false }, () => {
return 42;
});
const r = db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (fn()) VIRTUAL
)
`);
assert.strictEqual(r, undefined);
});
test('directOnly defaults to false', () => {
const db = new DatabaseSync(':memory:');
db.function('fn', { deterministic: true }, () => {
return 42;
});
const r = db.exec(`
CREATE TABLE t1 (
a INTEGER PRIMARY KEY,
b INTEGER GENERATED ALWAYS AS (fn()) VIRTUAL
)
`);
assert.strictEqual(r, undefined);
});
});
suite('return types', () => {
test('supported return types', () => {
const db = new DatabaseSync(':memory:');
db.function('retUndefined', () => {});
db.function('retNull', () => { return null; });
db.function('retNumber', () => { return 3; });
db.function('retString', () => { return 'foo'; });
db.function('retBigInt', () => { return 5n; });
db.function('retUint8Array', () => { return new Uint8Array([1, 2, 3]); });
db.function('retArrayBufferView', () => {
const arrayBuffer = new Uint8Array([1, 2, 3]).buffer;
return new DataView(arrayBuffer);
});
const stmt = db.prepare(`SELECT
retUndefined() AS retUndefined,
retNull() AS retNull,
retNumber() AS retNumber,
retString() AS retString,
retBigInt() AS retBigInt,
retUint8Array() AS retUint8Array,
retArrayBufferView() AS retArrayBufferView
`);
assert.deepStrictEqual(stmt.get(), {
__proto__: null,
retUndefined: null,
retNull: null,
retNumber: 3,
retString: 'foo',
retBigInt: 5,
retUint8Array: new Uint8Array([1, 2, 3]),
retArrayBufferView: new Uint8Array([1, 2, 3]),
});
});
test('throws if returned BigInt is too large for SQLite', () => {
const db = new DatabaseSync(':memory:');
db.function('retBigInt', () => {
return BigInt(Number.MAX_SAFE_INTEGER + 1);
});
const stmt = db.prepare('SELECT retBigInt() AS retBigInt');
assert.throws(() => {
stmt.get();
}, {
code: 'ERR_OUT_OF_RANGE',
});
});
test('does not support Promise return values', () => {
const db = new DatabaseSync(':memory:');
db.function('retPromise', async () => {});
const stmt = db.prepare('SELECT retPromise() AS retPromise');
assert.throws(() => {
stmt.get();
}, {
code: 'ERR_SQLITE_ERROR',
message: /Asynchronous user-defined functions are not supported/,
});
});
test('throws on unsupported return types', () => {
const db = new DatabaseSync(':memory:');
db.function('retFunction', () => {
return () => {};
});
const stmt = db.prepare('SELECT retFunction() AS retFunction');
assert.throws(() => {
stmt.get();
}, {
code: 'ERR_SQLITE_ERROR',
message: /Returned JavaScript value cannot be converted to a SQLite value/,
});
});
});
suite('handles conflicting errors from SQLite and JavaScript', () => {
test('throws if value cannot fit in a number', () => {
const db = new DatabaseSync(':memory:');
const expected = { __proto__: null, id: 5, data: 'foo' };
db.function('custom', (arg) => {});
db.exec('CREATE TABLE test (id NUMBER NOT NULL PRIMARY KEY, data TEXT)');
db.prepare('INSERT INTO test (id, data) VALUES (?, ?)').run(5, 'foo');
assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected);
assert.throws(() => {
db.exec(`UPDATE test SET data = CUSTOM(${Number.MAX_SAFE_INTEGER + 1})`);
}, {
code: 'ERR_OUT_OF_RANGE',
message: /Value is too large to be represented as a JavaScript number: 9007199254740992/,
});
assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected);
});
test('propagates JavaScript errors', () => {
const db = new DatabaseSync(':memory:');
const expected = { __proto__: null, id: 5, data: 'foo' };
const err = new Error('boom');
db.function('throws', () => {
throw err;
});
db.exec('CREATE TABLE test (id NUMBER NOT NULL PRIMARY KEY, data TEXT)');
db.prepare('INSERT INTO test (id, data) VALUES (?, ?)').run(5, 'foo');
assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected);
assert.throws(() => {
db.exec('UPDATE test SET data = THROWS()');
}, err);
assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected);
});
});
test('supported argument types', () => {
const db = new DatabaseSync(':memory:');
db.function('arguments', mustCall((i, f, s, n, b) => {
assert.strictEqual(i, 5);
assert.strictEqual(f, 3.14);
assert.strictEqual(s, 'foo');
assert.strictEqual(n, null);
assert.deepStrictEqual(b, new Uint8Array([254]));
return 42;
}));
const stmt = db.prepare(
'SELECT arguments(5, 3.14, \'foo\', null, x\'fe\') as result'
);
assert.deepStrictEqual(stmt.get(), { __proto__: null, result: 42 });
});
test('propagates thrown errors', () => {
const db = new DatabaseSync(':memory:');
const err = new Error('boom');
db.function('throws', () => {
throw err;
});
const stmt = db.prepare('SELECT throws()');
assert.throws(() => {
stmt.get();
}, err);
});
test('throws if database is not open', () => {
const db = new DatabaseSync(':memory:', { open: false });
assert.throws(() => {
db.function('foo', () => {});
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
});