playbook/outfitter-agents/plugins/outfitter/skills/bun-dev/references/sqlite-patterns.md

7.6 KiB

SQLite Patterns with bun:sqlite

Advanced patterns for database operations.

Migrations

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

class DatabasePool {
  private pools = new Map<string, Database>();

  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)

import { Database } from 'bun:sqlite';
import { createFactory } from 'hono/factory';

type Env = {
  Variables: {
    db: Database;
  };
};

const factory = createFactory<Env>();

// 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

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

// 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

// 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' }, ...]
// 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

// 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

// 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)');