Cache prepared statements per database connection
This commit is contained in:
@@ -391,6 +391,21 @@ export function db(): Database.Database {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache only runtime queries, after migrations have finished. Statements
|
||||||
|
// belong to this connection, so reopening the DB starts with an empty cache.
|
||||||
|
// Callers share statement options (e.g. raw()); use consistent options for
|
||||||
|
// identical SQL and pass values to run/get/all rather than binding permanently.
|
||||||
|
const statements = new Map<string, Database.Statement>();
|
||||||
|
const prepare = handle.prepare.bind(handle);
|
||||||
|
handle.prepare = ((sql: string) => {
|
||||||
|
let statement = statements.get(sql);
|
||||||
|
if (!statement) {
|
||||||
|
statement = prepare(sql);
|
||||||
|
statements.set(sql, statement);
|
||||||
|
}
|
||||||
|
return statement;
|
||||||
|
}) as typeof handle.prepare;
|
||||||
|
|
||||||
_db = handle;
|
_db = handle;
|
||||||
return _db;
|
return _db;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ beforeEach(async () => {
|
|||||||
vi.mocked(komodo.komodoUpdateTagsOnResource).mockResolvedValue({ ok: true });
|
vi.mocked(komodo.komodoUpdateTagsOnResource).mockResolvedValue({ ok: true });
|
||||||
const rcon = await import('../../src/lib/server/integrations/rcon.js');
|
const rcon = await import('../../src/lib/server/integrations/rcon.js');
|
||||||
vi.mocked(rcon.rconGetPlayerCount).mockResolvedValue(0);
|
vi.mocked(rcon.rconGetPlayerCount).mockResolvedValue(0);
|
||||||
vi.mocked(rcon.rconPlutoniumVersion).mockResolvedValue({ revision: 5354, error: null });
|
vi.mocked(rcon.rconPlutoniumVersion).mockImplementation(async () => {
|
||||||
|
// Version evidence must be observed strictly after the launch timestamp.
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2));
|
||||||
|
return { revision: 5354, error: null };
|
||||||
|
});
|
||||||
vi.mocked(rcon.rconStatusResult).mockImplementation(async server => status(server));
|
vi.mocked(rcon.rconStatusResult).mockImplementation(async server => status(server));
|
||||||
const { kick } = await import('../../src/lib/server/pipeline/worker.js');
|
const { kick } = await import('../../src/lib/server/pipeline/worker.js');
|
||||||
vi.mocked(kick).mockResolvedValue(undefined);
|
vi.mocked(kick).mockResolvedValue(undefined);
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||||
|
|
||||||
|
let fixture: Fixture;
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
fixture = await newFixture([]);
|
||||||
|
});
|
||||||
|
afterEach(async () => { await fixture.cleanup(); });
|
||||||
|
|
||||||
|
describe('prepared statement caching', () => {
|
||||||
|
it('reuses statements with fresh parameters and remains usable after rollback', async () => {
|
||||||
|
const { db } = await import('../../src/lib/server/db/index.js');
|
||||||
|
const insertSql = 'INSERT INTO settings (key, value) VALUES (?, ?)';
|
||||||
|
const selectSql = 'SELECT value FROM settings WHERE key = ?';
|
||||||
|
const insert = db().prepare(insertSql);
|
||||||
|
const select = db().prepare(selectSql);
|
||||||
|
insert.run('cache-first', 'one');
|
||||||
|
expect(() => db().transaction(() => {
|
||||||
|
db().prepare(insertSql).run('cache-rollback', 'discarded');
|
||||||
|
throw new Error('rollback');
|
||||||
|
})()).toThrow('rollback');
|
||||||
|
db().prepare(insertSql).run('cache-second', 'two');
|
||||||
|
|
||||||
|
expect(db().prepare(insertSql)).toBe(insert);
|
||||||
|
expect(db().prepare(selectSql)).toBe(select);
|
||||||
|
expect(select.get('cache-first')).toEqual({ value: 'one' });
|
||||||
|
expect(select.get('cache-second')).toEqual({ value: 'two' });
|
||||||
|
expect(select.get('cache-rollback')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prepares fresh statements when the connection is reopened', async () => {
|
||||||
|
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||||
|
const sql = 'SELECT value FROM settings WHERE key = ?';
|
||||||
|
const oldStatement = db().prepare(sql);
|
||||||
|
db().prepare('INSERT INTO settings (key, value) VALUES (?, ?)').run('cache-persisted', 'saved');
|
||||||
|
closeDb();
|
||||||
|
|
||||||
|
const statement = db().prepare(sql);
|
||||||
|
expect(statement).not.toBe(oldStatement);
|
||||||
|
expect(statement.database).not.toBe(oldStatement.database);
|
||||||
|
expect(statement.get('cache-persisted')).toEqual({ value: 'saved' });
|
||||||
|
expect(db().prepare(sql)).toBe(statement);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,7 +15,11 @@ vi.mock('../../src/lib/server/integrations/komodo.js', () => ({
|
|||||||
}));
|
}));
|
||||||
vi.mock('../../src/lib/server/integrations/rcon.js', () => ({
|
vi.mock('../../src/lib/server/integrations/rcon.js', () => ({
|
||||||
rconGetPlayerCount: vi.fn(async () => 0),
|
rconGetPlayerCount: vi.fn(async () => 0),
|
||||||
rconPlutoniumVersion: vi.fn(async () => ({ revision: 5354, error: null })),
|
rconPlutoniumVersion: vi.fn(async () => {
|
||||||
|
// Version evidence must be observed strictly after the launch timestamp.
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2));
|
||||||
|
return { revision: 5354, error: null };
|
||||||
|
}),
|
||||||
rconStatusResult: vi.fn(async (server: string) => ({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: '',
|
rconStatusResult: vi.fn(async (server: string) => ({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: '',
|
||||||
status: { players: 0, map: server.startsWith('t6_') ? 'zm_transit' : server.includes('_moon_') ? 'zombie_moon' : 'zombie_theater', gametype: null, roster: [] } }))
|
status: { players: 0, map: server.startsWith('t6_') ? 'zm_transit' : server.includes('_moon_') ? 'zombie_moon' : 'zombie_theater', gametype: null, roster: [] } }))
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user