# SQLite Patterns with bun:sqlite Advanced patterns for database operations. ## Migrations ```typescript const migrations = [ `CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY)`, `CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT UNIQUE)`, `ALTER TABLE users ADD COLUMN name TEXT`, `CREATE INDEX idx_users_email ON users(email)` ]; function getCurrentVersion(db: Database): number { try { const result = db.query('SELECT version FROM schema_version').get() as { version: number } | undefined; return result?.version || 0; } catch { return 0; } } function runMigrations(db: Database) { const currentVersion = getCurrentVersion(db); db.transaction(() => { for (let i = currentVersion; i < migrations.length; i++) { console.log(`Running migration ${i + 1}...`); db.run(migrations[i]); } db.run('DELETE FROM schema_version'); db.run('INSERT INTO schema_version (version) VALUES (?)', [migrations.length]); })(); console.log(`Migrated to version ${migrations.length}`); } ``` ## Connection Pool Pattern ```typescript class DatabasePool { private pools = new Map(); get(name: string = 'default'): Database { if (!this.pools.has(name)) { this.pools.set(name, new Database(`${name}.db`)); } return this.pools.get(name)!; } close(name?: string) { if (name) { this.pools.get(name)?.close(); this.pools.delete(name); } else { for (const db of this.pools.values()) { db.close(); } this.pools.clear(); } } } export const dbPool = new DatabasePool(); ``` ## Middleware Pattern (Hono) ```typescript import { Database } from 'bun:sqlite'; import { createFactory } from 'hono/factory'; type Env = { Variables: { db: Database; }; }; const factory = createFactory(); // Option 1: Per-request connection const dbMiddleware = factory.createMiddleware(async (c, next) => { const db = new Database('app.db'); c.set('db', db); try { await next(); } finally { db.close(); } }); // Option 2: Pooled connection (preferred for performance) const dbPoolMiddleware = factory.createMiddleware(async (c, next) => { const db = dbPool.get(); c.set('db', db); await next(); // Don't close — reuse connection }); ``` ## Repository Pattern ```typescript type User = { id: string; email: string; name: string; createdAt: Date; }; class UserRepository { constructor(private db: Database) {} private stmt = { findById: this.db.prepare('SELECT * FROM users WHERE id = ?'), findByEmail: this.db.prepare('SELECT * FROM users WHERE email = ?'), findAll: this.db.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT ?'), create: this.db.prepare('INSERT INTO users (id, email, name) VALUES (?, ?, ?) RETURNING *'), update: this.db.prepare('UPDATE users SET email = ?, name = ? WHERE id = ? RETURNING *'), delete: this.db.prepare('DELETE FROM users WHERE id = ? RETURNING *') }; findById(id: string): User | null { const row = this.stmt.findById.get(id); return row ? this.mapRow(row) : null; } findByEmail(email: string): User | null { const row = this.stmt.findByEmail.get(email); return row ? this.mapRow(row) : null; } findAll(limit = 100): User[] { const rows = this.stmt.findAll.all(limit); return rows.map(this.mapRow); } create(data: { email: string; name: string }): User { const id = crypto.randomUUID(); const row = this.stmt.create.get(id, data.email, data.name); return this.mapRow(row); } update(id: string, data: { email?: string; name?: string }): User | null { const existing = this.findById(id); if (!existing) return null; const row = this.stmt.update.get( data.email ?? existing.email, data.name ?? existing.name, id ); return this.mapRow(row); } delete(id: string): boolean { const row = this.stmt.delete.get(id); return !!row; } private mapRow(row: any): User { return { id: row.id, email: row.email, name: row.name, createdAt: new Date(row.created_at) }; } } ``` ## Transaction Patterns ```typescript // Simple transaction const transferFunds = db.transaction((fromId: string, toId: string, amount: number) => { const from = db.prepare('SELECT balance FROM accounts WHERE id = ?').get(fromId); if (!from || from.balance < amount) { throw new Error('Insufficient funds'); } db.run('UPDATE accounts SET balance = balance - ? WHERE id = ?', [amount, fromId]); db.run('UPDATE accounts SET balance = balance + ? WHERE id = ?', [amount, toId]); return { fromId, toId, amount }; }); // Nested transaction (savepoint) const complexOperation = db.transaction(() => { db.run('INSERT INTO orders (id) VALUES (?)', [orderId]); const addItem = db.transaction((itemId: string) => { db.run('INSERT INTO order_items (order_id, item_id) VALUES (?, ?)', [orderId, itemId]); }); for (const item of items) { addItem(item.id); // Each runs in savepoint } }); // Deferred vs immediate const deferredTx = db.transaction(() => { // Locks acquired on first write }).deferred(); const immediateTx = db.transaction(() => { // Locks acquired immediately }).immediate(); const exclusiveTx = db.transaction(() => { // Exclusive write lock }).exclusive(); ``` ## Query Helpers ```typescript // Reusable query object const getUserQuery = db.query('SELECT * FROM users WHERE id = ?'); const user1 = getUserQuery.get('1'); const user2 = getUserQuery.get('2'); // Values (array results) const allEmails = db.query('SELECT email FROM users').values(); // [['alice@example.com'], ['bob@example.com'], ...] // Named columns const users = db.query('SELECT id, email FROM users').all(); // [{ id: '1', email: 'alice@example.com' }, ...] ``` ## Full-Text Search ```typescript // Create FTS table db.run(` CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5( title, content, content='posts', content_rowid='id' ) `); // Triggers to keep FTS in sync db.run(` CREATE TRIGGER IF NOT EXISTS posts_ai AFTER INSERT ON posts BEGIN INSERT INTO posts_fts(rowid, title, content) VALUES (new.id, new.title, new.content); END `); // Search function searchPosts(query: string) { return db.prepare(` SELECT posts.* FROM posts JOIN posts_fts ON posts.id = posts_fts.rowid WHERE posts_fts MATCH ? ORDER BY rank LIMIT 20 `).all(query); } ``` ## JSON Support ```typescript // Store JSON db.run(` CREATE TABLE IF NOT EXISTS settings ( user_id TEXT PRIMARY KEY, preferences TEXT -- JSON stored as text ) `); // Insert JSON db.prepare('INSERT INTO settings VALUES (?, ?)').run( userId, JSON.stringify({ theme: 'dark', notifications: true }) ); // Query JSON (SQLite JSON functions) const darkUsers = db.prepare(` SELECT user_id FROM settings WHERE json_extract(preferences, '$.theme') = 'dark' `).all(); // Extract JSON field const theme = db.prepare(` SELECT json_extract(preferences, '$.theme') as theme FROM settings WHERE user_id = ? `).get(userId); ``` ## Performance Tips ```typescript // WAL mode for better concurrency db.run('PRAGMA journal_mode = WAL'); // Increase cache size db.run('PRAGMA cache_size = -64000'); // 64MB // Batch inserts const insertMany = db.transaction((users: User[]) => { const stmt = db.prepare('INSERT INTO users VALUES (?, ?, ?)'); for (const user of users) { stmt.run(user.id, user.email, user.name); } }); insertMany(thousandsOfUsers); // Much faster than individual inserts // Index for common queries db.run('CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)'); db.run('CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id)'); ```