bun-sqlite-orm
Version:
A lightweight TypeScript ORM for Bun runtime with Bun SQLite, featuring Active Record pattern and decorator-based entities
226 lines (207 loc) • 7.21 kB
text/typescript
import type { Database, Statement } from 'bun:sqlite';
import type { DbLogger, SQLQueryBindings } from './types';
/**
* Statement cache for optimizing prepared statement reuse.
*
* This class implements a caching layer for SQLite prepared statements to improve
* performance by avoiding the overhead of re-compiling frequently used queries.
*
* Benefits:
* - 30-50% performance improvement for repeated queries
* - Reduced memory allocations
* - Better resource utilization
* - Leverages bun:sqlite's optimized prepared statements
*/
// biome-ignore lint/complexity/noStaticOnlyClass: StatementCache needs to maintain state and provides a clear API namespace
export class StatementCache {
private static cache = new Map<string, Statement>();
private static hitCount = 0;
private static missCount = 0;
private static enabled = true;
private static testMode = false;
/**
* Get a cached statement or create a new one if not exists.
* This method is the core of the caching system.
*
* @param db Database connection
* @param sql SQL query string (used as cache key)
* @returns Prepared statement ready for execution
*/
static getStatement(db: Database, sql: string): Statement {
if (!StatementCache.enabled) {
// When caching is disabled, use db.prepare() directly
return db.prepare(sql);
}
if (StatementCache.cache.has(sql)) {
StatementCache.hitCount++;
const statement = StatementCache.cache.get(sql);
if (!statement) {
throw new Error(`Statement should exist in cache for SQL: ${sql}`);
}
return statement;
}
// Cache miss - create new statement
StatementCache.missCount++;
const statement = db.prepare(sql);
StatementCache.cache.set(sql, statement);
return statement;
}
/**
* Execute a query using cached statements.
* This is a convenience method that handles the complete flow.
*
* @param db Database connection
* @param sql SQL query string
* @param params Query parameters
* @param method Execution method ('get', 'all', or 'run')
* @returns Query result
*/
static executeQuery<T>(db: Database, sql: string, params: SQLQueryBindings[], method: 'get' | 'all' | 'run'): T {
// In test mode, use the database directly to support mocks
if (StatementCache.testMode) {
const statement = db.prepare(sql);
try {
return statement[method](...params) as T;
} finally {
statement.finalize();
}
}
const statement = StatementCache.getStatement(db, sql);
return statement[method](...params) as T;
}
/**
* Get cache statistics for monitoring and debugging.
*
* @returns Object containing cache metrics
*/
static getStats(): {
size: number;
hitCount: number;
missCount: number;
hitRate: number;
enabled: boolean;
} {
const total = StatementCache.hitCount + StatementCache.missCount;
return {
size: StatementCache.cache.size,
hitCount: StatementCache.hitCount,
missCount: StatementCache.missCount,
hitRate: total > 0 ? StatementCache.hitCount / total : 0,
enabled: StatementCache.enabled,
};
}
/**
* Enable or disable statement caching.
* When disabled, statements are created fresh each time.
*
* @param enabled Whether to enable caching
*/
static setEnabled(enabled: boolean): void {
StatementCache.enabled = enabled;
if (!enabled) {
// Clear cache when disabling to free resources
StatementCache.cleanup();
}
}
/**
* Enable or disable test mode.
* In test mode, statements are not cached and are finalized immediately
* to support database mocking in unit tests.
*
* @param testMode Whether to enable test mode
*/
static setTestMode(testMode: boolean): void {
StatementCache.testMode = testMode;
if (testMode) {
// Clear cache when entering test mode
StatementCache.cleanup();
}
}
/**
* Clear specific statements from cache by SQL pattern.
* Useful for invalidating cache when schema changes.
*
* @param sqlPattern Regular expression or string to match SQL queries
*/
static invalidate(sqlPattern: string | RegExp): number {
let removed = 0;
const pattern = typeof sqlPattern === 'string' ? new RegExp(sqlPattern) : sqlPattern;
for (const [sql, statement] of StatementCache.cache.entries()) {
if (pattern.test(sql)) {
statement.finalize();
StatementCache.cache.delete(sql);
removed++;
}
}
return removed;
}
/**
* Clean up all cached statements and clear the cache.
* This should be called when the database connection is closed
* or when the application shuts down.
*/
static cleanup(): void {
for (const statement of StatementCache.cache.values()) {
try {
statement.finalize();
} catch (error) {
// Statement may already be finalized
// This can happen if the database connection was closed
}
}
StatementCache.cache.clear();
StatementCache.hitCount = 0;
StatementCache.missCount = 0;
}
/**
* Get the current cache size (number of cached statements).
* Useful for monitoring memory usage.
*
* @returns Number of statements in cache
*/
static size(): number {
return StatementCache.cache.size;
}
/**
* Check if a specific SQL query is cached.
* Useful for testing and debugging.
*
* @param sql SQL query string
* @returns Whether the query is cached
*/
static has(sql: string): boolean {
return StatementCache.cache.has(sql);
}
/**
* Log cache statistics using the provided logger.
* Useful for performance monitoring and debugging.
*
* @param logger Database logger instance
*/
static logStats(logger: DbLogger): void {
const stats = StatementCache.getStats();
logger.info('Statement cache statistics', {
cacheSize: stats.size,
hitCount: stats.hitCount,
missCount: stats.missCount,
hitRate: `${(stats.hitRate * 100).toFixed(2)}%`,
enabled: stats.enabled,
});
}
/**
* Reset cache statistics without clearing the cache.
* Useful for periodic monitoring.
*/
static resetStats(): void {
StatementCache.hitCount = 0;
StatementCache.missCount = 0;
}
/**
* Get all cached SQL queries for debugging purposes.
*
* @returns Array of cached SQL strings
*/
static getCachedQueries(): string[] {
return Array.from(StatementCache.cache.keys());
}
}