# SQLite CRUD Patterns Complete examples for database operations with bun:sqlite. ## Basic Repository ```typescript import { Database } from 'bun:sqlite'; type User = { id: string; email: string; name: string; createdAt: Date; }; type UserRow = { id: string; email: string; name: string; created_at: string; }; class UserRepository { private stmt: { findById: ReturnType; findByEmail: ReturnType; findAll: ReturnType; create: ReturnType; update: ReturnType; delete: ReturnType; }; constructor(private db: Database) { // Initialize schema this.db.run(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) `); // Prepare statements once this.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) as UserRow | null; return row ? this.mapRow(row) : null; } findByEmail(email: string): User | null { const row = this.stmt.findByEmail.get(email) as UserRow | null; return row ? this.mapRow(row) : null; } findAll(limit = 100): User[] { const rows = this.stmt.findAll.all(limit) as UserRow[]; return rows.map(row => this.mapRow(row)); } create(data: { email: string; name: string }): User { const id = crypto.randomUUID(); const row = this.stmt.create.get(id, data.email, data.name) as UserRow; 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 ) as UserRow; return this.mapRow(row); } delete(id: string): boolean { const row = this.stmt.delete.get(id); return row !== null; } private mapRow(row: UserRow): User { return { id: row.id, email: row.email, name: row.name, createdAt: new Date(row.created_at) }; } } ``` ## Usage ```typescript const db = new Database('app.db'); const users = new UserRepository(db); // Create const user = users.create({ email: 'alice@example.com', name: 'Alice' }); console.log('Created:', user.id); // Read const found = users.findById(user.id); console.log('Found:', found?.email); // Update const updated = users.update(user.id, { name: 'Alice Smith' }); console.log('Updated:', updated?.name); // Delete const deleted = users.delete(user.id); console.log('Deleted:', deleted); // List const allUsers = users.findAll(10); console.log('All users:', allUsers.length); db.close(); ``` ## With Transactions ```typescript class AccountRepository { constructor(private db: Database) { this.db.run(` CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, balance INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id) ) `); } transfer = this.db.transaction((fromId: string, toId: string, amount: number) => { // Check balance const from = this.db.prepare('SELECT balance FROM accounts WHERE id = ?').get(fromId) as { balance: number } | null; if (!from) { throw new Error('Source account not found'); } if (from.balance < amount) { throw new Error('Insufficient funds'); } // Debit this.db.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?').run(amount, fromId); // Credit this.db.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?').run(amount, toId); return { fromId, toId, amount }; }); bulkCreate = this.db.transaction((accounts: Array<{ userId: string; balance: number }>) => { const stmt = this.db.prepare('INSERT INTO accounts (id, user_id, balance) VALUES (?, ?, ?)'); const created = []; for (const account of accounts) { const id = crypto.randomUUID(); stmt.run(id, account.userId, account.balance); created.push(id); } return created; }); } ``` ## Pagination ```typescript type PaginatedResult = { items: T[]; total: number; page: number; pageSize: number; totalPages: number; }; class UserRepository { // ... other methods findPaginated(page: number, pageSize: number): PaginatedResult { const offset = (page - 1) * pageSize; const countResult = this.db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }; const total = countResult.count; const rows = this.db.prepare(` SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ? `).all(pageSize, offset) as UserRow[]; return { items: rows.map(row => this.mapRow(row)), total, page, pageSize, totalPages: Math.ceil(total / pageSize) }; } } // Usage const result = users.findPaginated(1, 20); console.log(`Page ${result.page} of ${result.totalPages}`); console.log(`Showing ${result.items.length} of ${result.total} users`); ``` ## Search with Full-Text ```typescript class PostRepository { constructor(private db: Database) { // Create main table this.db.run(` CREATE TABLE IF NOT EXISTS posts ( id TEXT PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, user_id TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) `); // Create FTS index this.db.run(` CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5( title, content, content='posts', content_rowid='rowid' ) `); // Triggers to keep FTS in sync this.db.run(` CREATE TRIGGER IF NOT EXISTS posts_ai AFTER INSERT ON posts BEGIN INSERT INTO posts_fts(rowid, title, content) VALUES (new.rowid, new.title, new.content); END `); this.db.run(` CREATE TRIGGER IF NOT EXISTS posts_ad AFTER DELETE ON posts BEGIN INSERT INTO posts_fts(posts_fts, rowid, title, content) VALUES ('delete', old.rowid, old.title, old.content); END `); this.db.run(` CREATE TRIGGER IF NOT EXISTS posts_au AFTER UPDATE ON posts BEGIN INSERT INTO posts_fts(posts_fts, rowid, title, content) VALUES ('delete', old.rowid, old.title, old.content); INSERT INTO posts_fts(rowid, title, content) VALUES (new.rowid, new.title, new.content); END `); } search(query: string, limit = 20) { return this.db.prepare(` SELECT posts.*, bm25(posts_fts) as rank FROM posts JOIN posts_fts ON posts.rowid = posts_fts.rowid WHERE posts_fts MATCH ? ORDER BY rank LIMIT ? `).all(query, limit); } } // Usage const posts = new PostRepository(db); const results = posts.search('typescript tutorial'); ``` ## JSON Storage ```typescript type Settings = { theme: 'light' | 'dark'; notifications: boolean; language: string; }; class SettingsRepository { constructor(private db: Database) { this.db.run(` CREATE TABLE IF NOT EXISTS settings ( user_id TEXT PRIMARY KEY, data TEXT NOT NULL ) `); } get(userId: string): Settings | null { const row = this.db.prepare('SELECT data FROM settings WHERE user_id = ?').get(userId) as { data: string } | null; if (!row) return null; return JSON.parse(row.data); } set(userId: string, settings: Settings): void { this.db.prepare(` INSERT INTO settings (user_id, data) VALUES (?, ?) ON CONFLICT (user_id) DO UPDATE SET data = excluded.data `).run(userId, JSON.stringify(settings)); } update(userId: string, partial: Partial): Settings | null { const existing = this.get(userId); if (!existing) return null; const updated = { ...existing, ...partial }; this.set(userId, updated); return updated; } // Query JSON fields directly findByTheme(theme: 'light' | 'dark') { return this.db.prepare(` SELECT user_id FROM settings WHERE json_extract(data, '$.theme') = ? `).all(theme); } } ``` ## With Hono API ```typescript import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; import { z } from 'zod'; import { Database } from 'bun:sqlite'; import { createFactory } from 'hono/factory'; import { HTTPException } from 'hono/http-exception'; type Env = { Variables: { db: Database; users: UserRepository; }; }; const factory = createFactory(); const dbMiddleware = factory.createMiddleware(async (c, next) => { const db = new Database('app.db'); const users = new UserRepository(db); c.set('db', db); c.set('users', users); try { await next(); } finally { db.close(); } }); const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(1).max(100) }); const UpdateUserSchema = z.object({ email: z.string().email().optional(), name: z.string().min(1).max(100).optional() }); const app = factory.createApp() .use('*', dbMiddleware) .get('/users', (c) => { const users = c.get('users'); const limit = Number(c.req.query('limit')) || 20; return c.json({ users: users.findAll(limit) }); }) .get('/users/:id', (c) => { const users = c.get('users'); const user = users.findById(c.req.param('id')); if (!user) { throw new HTTPException(404, { message: 'User not found' }); } return c.json({ user }); }) .post('/users', zValidator('json', CreateUserSchema), (c) => { const users = c.get('users'); const data = c.req.valid('json'); const existing = users.findByEmail(data.email); if (existing) { throw new HTTPException(409, { message: 'Email already registered' }); } const user = users.create(data); return c.json({ user }, 201); }) .patch('/users/:id', zValidator('json', UpdateUserSchema), (c) => { const users = c.get('users'); const data = c.req.valid('json'); const user = users.update(c.req.param('id'), data); if (!user) { throw new HTTPException(404, { message: 'User not found' }); } return c.json({ user }); }) .delete('/users/:id', (c) => { const users = c.get('users'); const deleted = users.delete(c.req.param('id')); if (!deleted) { throw new HTTPException(404, { message: 'User not found' }); } return c.json({ deleted: true }); }); export default app; ```