548 lines
14 KiB
TypeScript
548 lines
14 KiB
TypeScript
import { describe, expect, test, beforeEach, afterEach, vi } from 'vitest';
|
|
|
|
import Database, { setLogger } from '../lib/index.js';
|
|
|
|
const rows = [
|
|
{
|
|
a: 1,
|
|
b: '123',
|
|
c: Buffer.from('abba', 'hex'),
|
|
},
|
|
{
|
|
a: 2,
|
|
b: '456',
|
|
c: Buffer.from('dada', 'hex'),
|
|
},
|
|
{
|
|
a: 3,
|
|
b: '789',
|
|
c: null,
|
|
},
|
|
];
|
|
|
|
let db: Database;
|
|
beforeEach(() => {
|
|
db = new Database();
|
|
|
|
db.exec(`
|
|
CREATE TABLE t (a INTEGER, b TEXT, c BLOB);
|
|
|
|
INSERT INTO t (a, b, c) VALUES
|
|
(1, '123', x'abba'),
|
|
(2, '456', x'dada'),
|
|
(3, '789', NULL);
|
|
`);
|
|
});
|
|
|
|
afterEach(() => {
|
|
db.close();
|
|
});
|
|
|
|
test('db.close', () => {
|
|
db.close();
|
|
expect(() => db.close()).toThrowError('Database already closed');
|
|
|
|
expect(() => db.exec('')).toThrowError('Database closed');
|
|
expect(() => db.prepare('')).toThrowError('Database closed');
|
|
|
|
// Just to fix afterEach
|
|
db = new Database();
|
|
});
|
|
|
|
test('db.close with existing statement', () => {
|
|
const stmt = db.prepare('SELECT 1');
|
|
db.close();
|
|
|
|
expect(() => stmt.run()).toThrowError('Statement closed');
|
|
|
|
// Just to fix afterEach
|
|
db = new Database();
|
|
});
|
|
|
|
test('statement.close', () => {
|
|
const stmt = db.prepare('SELECT 1');
|
|
stmt.close();
|
|
|
|
expect(() => stmt.close()).toThrowError('Statement already closed');
|
|
});
|
|
|
|
test('statement.run', () => {
|
|
expect(db.prepare('SELECT * FROM t').run()).toEqual({
|
|
changes: 0,
|
|
lastInsertRowid: 3,
|
|
});
|
|
|
|
expect(
|
|
db.prepare(`INSERT INTO t (a, b, c) VALUES (4, '4', NULL)`).run(),
|
|
).toEqual({
|
|
changes: 1,
|
|
lastInsertRowid: 4,
|
|
});
|
|
});
|
|
|
|
test('statement.run after close', () => {
|
|
const stmt = db.prepare('SELECT 1');
|
|
stmt.close();
|
|
expect(() => stmt.run()).toThrowError('Statement closed');
|
|
});
|
|
|
|
test('statement.get', () => {
|
|
expect(
|
|
db
|
|
.prepare('SELECT * FROM t')
|
|
.get<{ a: number; b: string; c: Uint8Array }>(),
|
|
).toEqual(rows[0]);
|
|
});
|
|
|
|
test('statement.get after close', () => {
|
|
const stmt = db.prepare('SELECT 1');
|
|
stmt.close();
|
|
expect(() => stmt.get()).toThrowError('Statement closed');
|
|
});
|
|
|
|
test('statement.all', () => {
|
|
expect(db.prepare('SELECT * FROM t').all()).toEqual(rows);
|
|
});
|
|
|
|
test('statement.all after close', () => {
|
|
const stmt = db.prepare('SELECT 1');
|
|
stmt.close();
|
|
expect(() => stmt.all()).toThrowError('Statement closed');
|
|
});
|
|
|
|
test('statement.get persistent=true', () => {
|
|
expect(db.prepare('SELECT * FROM t', { persistent: true }).get()).toEqual(
|
|
rows[0],
|
|
);
|
|
});
|
|
|
|
test('statement.get persistent=true with undefined', () => {
|
|
db.exec('DELETE FROM t');
|
|
expect(
|
|
db.prepare('SELECT * FROM t', { persistent: true }).get(),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
test('statement.all persistent=true', () => {
|
|
expect(db.prepare('SELECT * FROM t', { persistent: true }).all()).toEqual(
|
|
rows,
|
|
);
|
|
});
|
|
|
|
test('statement.get pluck=true', () => {
|
|
expect(db.prepare('SELECT a FROM t', { pluck: true }).get()).toEqual(1);
|
|
});
|
|
|
|
test('statement.all pluck=true', () => {
|
|
expect(db.prepare('SELECT a FROM t', { pluck: true }).all()).toEqual([
|
|
1, 2, 3,
|
|
]);
|
|
});
|
|
|
|
test('statement.get persistent=true, pluck=true', () => {
|
|
expect(
|
|
db.prepare('SELECT a FROM t', { persistent: true, pluck: true }).get(),
|
|
).toEqual(1);
|
|
});
|
|
|
|
test('statement.all persistent=true, pluck=true', () => {
|
|
expect(
|
|
db.prepare('SELECT a FROM t', { persistent: true, pluck: true }).all(),
|
|
).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
test('pragma', () => {
|
|
db.pragma('user_version = 123');
|
|
expect(db.pragma('user_version')).toEqual([{ user_version: 123 }]);
|
|
});
|
|
|
|
test('smple pragma', () => {
|
|
db.pragma('user_version = 123');
|
|
expect(db.pragma('user_version', { simple: true })).toEqual(123);
|
|
});
|
|
|
|
test('too many columns for pluck', () => {
|
|
const stmt = db.prepare('SELECT * FROM t', { pluck: true });
|
|
expect(() => stmt.get()).toThrowError('Invalid column count for pluck');
|
|
});
|
|
|
|
test('persistent statement recompilation', () => {
|
|
const stmt = db.prepare('SELECT * FROM t', { persistent: true });
|
|
expect(stmt.get()).toEqual(rows[0]);
|
|
|
|
db.exec(`ALTER TABLE t ADD COLUMN d TEXT DEFAULT 'hello'`);
|
|
|
|
expect(stmt.get()).toEqual({
|
|
a: 1,
|
|
b: '123',
|
|
c: Buffer.from('abba', 'hex'),
|
|
d: 'hello',
|
|
});
|
|
});
|
|
|
|
test('setLogger should log on statement recompilation', () => {
|
|
const messages = new Array<{ code: string; message: string }>();
|
|
setLogger((code, message) => messages.push({ code, message }));
|
|
|
|
const stmt = db.prepare('SELECT * FROM t', { persistent: true });
|
|
|
|
db.exec(`ALTER TABLE t ADD COLUMN d TEXT DEFAULT 'hello'`);
|
|
|
|
expect(stmt.get()).not.toBeUndefined();
|
|
|
|
expect(messages).toHaveLength(1);
|
|
expect(messages[0]?.code).toEqual('SQLITE_SCHEMA');
|
|
expect(messages[0]?.message).toMatch(/database schema has changed/);
|
|
});
|
|
|
|
describe('list parameters', () => {
|
|
test('correct count', () => {
|
|
expect(db.prepare('SELECT * FROM t WHERE a > ?').get([2])).toEqual(rows[2]);
|
|
});
|
|
|
|
test('incorrect count', () => {
|
|
const stmt = db.prepare('SELECT * FROM t WHERE a > ?');
|
|
expect(() => stmt.get([2, 3])).toThrowError('Expected 1 parameters, got 2');
|
|
});
|
|
|
|
test('absent parameters', () => {
|
|
const stmt = db.prepare('SELECT * FROM t WHERE a > ?');
|
|
expect(() => stmt.get()).toThrowError('Expected 1 parameters, got 0');
|
|
});
|
|
|
|
test('object parameters', () => {
|
|
const stmt = db.prepare('SELECT * FROM t WHERE a > ?');
|
|
expect(() => stmt.get({})).toThrowError(
|
|
'Query requires an array of anonymous params',
|
|
);
|
|
});
|
|
|
|
test('against named parameters', () => {
|
|
const stmt = db.prepare('SELECT * FROM t WHERE a > $a');
|
|
expect(() => stmt.get([2])).toThrowError(
|
|
'Query requires an object of named params',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('object parameters', () => {
|
|
test('correct count', () => {
|
|
expect(db.prepare('SELECT * FROM t WHERE a > $a').get({ a: 2 })).toEqual(
|
|
rows[2],
|
|
);
|
|
});
|
|
|
|
test('undefined param', () => {
|
|
const stmt = db.prepare('SELECT * FROM t WHERE a > $a');
|
|
expect(() => stmt.get({})).toThrowError(
|
|
'Failed to bind param a, error unexpected type `undefined`',
|
|
);
|
|
});
|
|
|
|
test('absent parameters', () => {
|
|
const stmt = db.prepare('SELECT * FROM t WHERE a > $a');
|
|
expect(() => stmt.get()).toThrowError('Expected 1 parameters, got 0');
|
|
});
|
|
});
|
|
|
|
describe('tail', () => {
|
|
test('allow trailing --', () => {
|
|
db.prepare('SELECT 1; --');
|
|
});
|
|
|
|
test('allow trailing /*', () => {
|
|
db.prepare('SELECT 1; /*');
|
|
});
|
|
|
|
test('disallow statement after comments', () => {
|
|
expect(() =>
|
|
db.prepare('SELECT 1; -- asdfasdf\n/*\n*/SELECT 2'),
|
|
).toThrowError("Can't prepare more than one statement");
|
|
});
|
|
|
|
test('disallow trailing /', () => {
|
|
expect(() => db.prepare('SELECT 1; /')).toThrowError(
|
|
"Can't prepare more than one statement",
|
|
);
|
|
});
|
|
|
|
test('disallow trailing -', () => {
|
|
expect(() => db.prepare('SELECT 1; -')).toThrowError(
|
|
"Can't prepare more than one statement",
|
|
);
|
|
});
|
|
});
|
|
|
|
test('invalid null params', () => {
|
|
const stmt = db.prepare('SELECT 1');
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect(() => stmt.get(null as any)).toThrowError('Params cannot be null');
|
|
});
|
|
|
|
test('invalid params', () => {
|
|
const stmt = db.prepare('SELECT 1');
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect(() => stmt.get(123 as any)).toThrowError(
|
|
'Params must be either object or array',
|
|
);
|
|
});
|
|
|
|
test('invalid database path', () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect(() => new Database(123 as any)).toThrowError('Invalid database path');
|
|
});
|
|
|
|
test('invalid exec query', () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect(() => db.exec(123 as any)).toThrowError('Invalid sql argument');
|
|
});
|
|
|
|
test('invalid prepare query', () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect(() => db.prepare(123 as any)).toThrowError('Invalid query argument');
|
|
});
|
|
|
|
test('invalid pragma query', () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect(() => db.pragma(123 as any)).toThrowError('Invalid pragma argument');
|
|
});
|
|
|
|
describe('transaction', () => {
|
|
test('commit', () => {
|
|
db.transaction(() => {
|
|
db.prepare(`INSERT INTO t (a, b) VALUES (42, 'success')`).run();
|
|
})();
|
|
|
|
expect(
|
|
db.prepare('SELECT b FROM t WHERE a IS 42', { pluck: true }).get(),
|
|
).toEqual('success');
|
|
});
|
|
|
|
test('rollback', () => {
|
|
db.prepare(`INSERT INTO t (a, b) VALUES (42, 'success')`).run();
|
|
|
|
expect(() =>
|
|
db.transaction(() => {
|
|
db.prepare(`UPDATE t SET b = 'fail' WHERE A is 42`).run();
|
|
throw new Error('rollback');
|
|
})(),
|
|
).toThrowError('rollback');
|
|
|
|
expect(
|
|
db.prepare('SELECT b FROM t WHERE a IS 42', { pluck: true }).get(),
|
|
).toEqual('success');
|
|
});
|
|
|
|
test('nested rollback', () => {
|
|
db.transaction(() => {
|
|
db.prepare(`INSERT INTO t (a, b) VALUES (42, 'success')`).run();
|
|
|
|
expect(() =>
|
|
db.transaction(() => {
|
|
db.prepare(`UPDATE t SET b = 'fail' WHERE A is 42`).run();
|
|
throw new Error('rollback');
|
|
})(),
|
|
).toThrowError('rollback');
|
|
})();
|
|
|
|
expect(
|
|
db.prepare('SELECT b FROM t WHERE a IS 42', { pluck: true }).get(),
|
|
).toEqual('success');
|
|
});
|
|
});
|
|
|
|
test('single-copy strings', () => {
|
|
db.exec(`
|
|
DROP TABLE t;
|
|
|
|
CREATE TABLE t (rowid INTEGER PRIMARY KEY NOT NULL, value TEXT NOT NULL);
|
|
|
|
INSERT INTO t (value) VALUES ('0a'), ('0a'), ('0a'), ('0a'), ('0a');
|
|
`);
|
|
|
|
expect(
|
|
db
|
|
.prepare('SELECT rowid FROM t WHERE value IS ?', {
|
|
pluck: true,
|
|
})
|
|
.all(['0a']),
|
|
).toEqual([1, 2, 3, 4, 5]);
|
|
});
|
|
|
|
test('number mode', () => {
|
|
db.exec(
|
|
`
|
|
DELETE FROM t;
|
|
-- MAX_INT64
|
|
INSERT INTO t (a) VALUES (1152921504606846975);
|
|
`,
|
|
);
|
|
|
|
expect(db.prepare('SELECT a FROM t', { pluck: true }).get()).toEqual(
|
|
1152921504606847000,
|
|
);
|
|
});
|
|
|
|
test('bigint mode', () => {
|
|
db.exec(`
|
|
DELETE FROM t;
|
|
`);
|
|
|
|
const n = 0x7fff_ffff_ffff_ffffn;
|
|
|
|
db.prepare(
|
|
`
|
|
INSERT INTO t (a) VALUES (?);
|
|
`,
|
|
{ bigint: true },
|
|
).run([n]);
|
|
|
|
expect(
|
|
db.prepare('SELECT a FROM t', { pluck: true, bigint: true }).get(),
|
|
).toEqual(n);
|
|
});
|
|
|
|
test('extended error codes', () => {
|
|
db.exec(`
|
|
CREATE TABLE parent (id INTEGER PRIMARY KEY);
|
|
|
|
CREATE TABLE refs (id INTEGER, FOREIGN KEY (id) REFERENCES parent(id));
|
|
`);
|
|
|
|
const stmt = db.prepare('INSERT INTO refs (id) VALUES (?)');
|
|
|
|
expect(() => stmt.run([4])).toThrowError(
|
|
expect.objectContaining({
|
|
message:
|
|
'sqlite error(SQLITE_CONSTRAINT_FOREIGNKEY): ' +
|
|
'FOREIGN KEY constraint failed',
|
|
code: 'SQLITE_CONSTRAINT_FOREIGNKEY',
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('tokenizer setup', () => {
|
|
db.initTokenizer();
|
|
});
|
|
|
|
test('tokenizer setup after close', () => {
|
|
db.close();
|
|
expect(() => db.initTokenizer()).toThrowError('Database closed');
|
|
|
|
// Just to fix afterEach
|
|
db = new Database();
|
|
});
|
|
|
|
test('signalTokenize', () => {
|
|
expect(db.signalTokenize('a b c')).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
test('invalid argument for signalTokenize', () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect(() => db.signalTokenize(123 as any)).toThrowError('Invalid value');
|
|
});
|
|
|
|
test('does not cache statements', () => {
|
|
expect(db.prepare('SELECT 1')).not.toBe(db.prepare('SELECT 1'));
|
|
});
|
|
|
|
describe('statement cache', () => {
|
|
let cachedDb: Database;
|
|
beforeEach(() => {
|
|
cachedDb = new Database(':memory:', { cacheStatements: true });
|
|
|
|
cachedDb.exec(`
|
|
CREATE TABLE t (a INTEGER, b TEXT, c BLOB);
|
|
|
|
INSERT INTO t (a, b, c) VALUES
|
|
(1, '123', x'abba'),
|
|
(2, '456', x'dada'),
|
|
(3, '789', NULL);
|
|
`);
|
|
});
|
|
|
|
afterEach(() => {
|
|
cachedDb.close();
|
|
});
|
|
|
|
test('caches statements', () => {
|
|
expect(cachedDb.prepare('SELECT 1')).toBe(cachedDb.prepare('SELECT 1'));
|
|
});
|
|
|
|
test('uses query in cache key', () => {
|
|
expect(cachedDb.prepare('SELECT 1')).not.toBe(cachedDb.prepare('SELECT 2'));
|
|
});
|
|
|
|
test('uses pluck in cache key', () => {
|
|
expect(cachedDb.prepare('SELECT 1')).not.toBe(
|
|
cachedDb.prepare('SELECT 1', { pluck: true }),
|
|
);
|
|
});
|
|
|
|
test('uses bigint in cache key', () => {
|
|
expect(cachedDb.prepare('SELECT 1')).not.toBe(
|
|
cachedDb.prepare('SELECT 1', { bigint: true }),
|
|
);
|
|
});
|
|
|
|
test('invalidates cache on close', () => {
|
|
const stmt = cachedDb.prepare('SELECT 1');
|
|
stmt.close();
|
|
expect(stmt).not.toBe(cachedDb.prepare('SELECT 1'));
|
|
});
|
|
|
|
test('does not cache persistent=false statements', () => {
|
|
expect(cachedDb.prepare('SELECT 1', { persistent: false })).not.toBe(
|
|
cachedDb.prepare('SELECT 1', { persistent: false }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('custom function', () => {
|
|
let fnDb: Database;
|
|
let fn: ReturnType<typeof vi.fn>;
|
|
let bigFn: ReturnType<typeof vi.fn>;
|
|
beforeEach(() => {
|
|
fnDb = new Database(':memory:');
|
|
|
|
fn = vi.fn();
|
|
fnDb.createFunction('fn', fn);
|
|
|
|
bigFn = vi.fn();
|
|
fnDb.createFunction('bigFn', bigFn, {
|
|
bigint: true,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
fnDb.close();
|
|
});
|
|
|
|
test('it calls the function without args', () => {
|
|
fnDb.exec(`SELECT fn()`);
|
|
expect(fn).toHaveBeenCalledWith();
|
|
});
|
|
|
|
test('it calls the function with multiple args', () => {
|
|
fnDb.exec(`SELECT fn(1, '123', NULL)`);
|
|
expect(fn).toHaveBeenCalledWith(1, '123', null);
|
|
});
|
|
|
|
test('it calls the function with blob', () => {
|
|
fnDb.exec(`SELECT fn(x'abba')`);
|
|
expect(fn).toHaveBeenCalledWith(Buffer.from('abba', 'hex'));
|
|
});
|
|
|
|
test('it uses bigints when configured', () => {
|
|
fnDb.exec(`SELECT bigFn(123456)`);
|
|
expect(bigFn).toHaveBeenCalledWith(123456n);
|
|
});
|
|
|
|
test('it throws when function returns a value', () => {
|
|
fnDb.createFunction('intFn', () => {
|
|
return 1;
|
|
});
|
|
expect(() => fnDb.exec(`SELECT intFn()`)).toThrowError('SQLITE_ERROR');
|
|
});
|
|
});
|