Imported from jackmaxwil/alfred (
packages/db/AGENTS.md). Install upstream withnpx skills add jackmaxwil/alfred --skill db. Copyright stays with the author.
Database Rules
- Source of truth. Migrations in
packages/db/src/migrationsdefine the schema. Never edit generated SQL directly in production environments—add new migrations, and mirror new features (e.g., eval definitions/runs/scores) with dedicated SQL files. - Vector support. Local Postgres runs via
pgvector/pgvector:pg16. KeepCREATE EXTENSION IF NOT EXISTS vectorin the earliest migration and verify with\dx. When defining columns, importvectorfromdrizzle-orm/pg-core(notdrizzle-orm-pgvector/pg) to avoid SSR bundling issues in the web app. - Idempotent migrations. Wrap DDL in
IF NOT EXISTS/IF EXISTSwhen safe. Non-idempotent operations must document irreversible effects. - Index discipline. Composite indexes should match the repo query predicates. When adding new queries, expand
0008_indexes.sqlor subsequent migrations accordingly. Eval tables must index(def_id, dataset_id, started_at)for run listings and(run_id, point_id, scorer)for score lookups, as seen in0012_evals.sql. Use partial indexes (WHERE column IS NOT NULL) to reduce index size for sparse columns. - Migration runner. Use
packages/db/scripts/migrate.tseverywhere (CI, local dev). It records applied migrations in_migrations. - Schema sync. Keep Drizzle schema files (
packages/db/src/schema/*.ts) aligned with migrations. Vector dimensions must useEMBEDDING_DIMfrom@alfred/embed(single source of truth). When changing vector dimensions, drop indexes beforeALTER COLUMN TYPE, recreate withIF NOT EXISTS, and document that existing embeddings become NULL. - Testing. Write Vitest suites under
packages/db/testthat spin up an isolated database schema and assert repo behaviour (notes, reminders, timers, eval runs/scores, etc.). - Laminar correlation. Columns like
laminar_eval_idbelong in the primary run table to enable dual-write correlation. Always backfill withALTER TABLE ... ADD COLUMN IF NOT EXISTSmigrations so replays remain idempotent. - Transactions. Use
db.transaction()for multi-step operations that must be atomic. Transactions automatically rollback on error. Use for operations that must succeed or fail together. PostgreSQL reserves a dedicated connection from the pool—keep transactions short to avoid connection exhaustion. - Batch operations. Use
db.batch()for multiple independent queries (Drizzle batch API). Batch operations execute sequentially in a single round-trip. Use for independent queries that don't require atomicity. - Savepoints. Use savepoints for partial rollbacks within transactions. Call
tx.savepoint()inside a transaction to create a nested transaction that can rollback independently while the outer transaction continues. - Query performance. All repo queries must complete in <10ms (p99). Instrument with metrics before optimizing.
- Connection pooling. PostgreSQL transactions reserve connections. Avoid long-running transactions to prevent connection exhaustion.
- Bulk updates. Prefer batch updates with
Promise.all+ chunks (size 10-50) overdb.transactionor sequential loops for high-volume writes. UseUPDATE ... FROM (VALUES ...)for massive updates if possible.
Drizzle Query Patterns
Core Principle
Use Drizzle ORM's type-safe query builder consistently. Leverage TypeScript inference, optimize with indexes, and avoid anti-patterns that reduce type safety or performance.
Rules
-
Type-safe queries. Always use Drizzle's query builder (
db.select().from(table).where(...)), never raw SQL (db.execute(sql...)). -
Query inference. Use
typeof table.$inferSelectandtypeof table.$inferInsertfor types. -
Index usage. Match query predicates to composite indexes. Use
and()/or()for compound conditions. -
Batch operations. Use
db.batch([...])for multiple independent queries instead of loops. -
Transactions. Use
db.transaction(async (tx) => {...})for atomic operations. Keep transactions short. -
Returning clauses. Use
.returning()to get inserted/updated rows instead of separate SELECT queries. -
JSONB handling. Use
as anyfor JSONB fields (Drizzle limitation). Document this pattern in code comments. -
Safe tsquery generation. When constructing
tsqueryfor search, always useplainto_tsquery('english', ...)for user input orsql.join(..., sql||)for combining queries. Never string-template raw variables intoto_tsquerywithout sanitization. -
Performance. All queries must complete in <10ms (p99). Use indexes for all WHERE clauses. Prefer batch operations over loops. Keep transactions short (<100ms).
-
Bulk updates. For bulk updates of the same column (e.g., confidence decay), prefer single SQL
UPDATE ... FROM (VALUES ...)statement overPromise.allloops. This reduces DB roundtrips and improves performance. -
SQL-level JSON filtering. When filtering rows by JSONB properties, use SQL-level filtering (
sql\json_extract(column, '$.path') LIKE '%pattern%'``) instead of fetching all rows and filtering in memory. This reduces data transfer and improves performance.
Migration Guide
- Create new files using the next sequential number (
0011_name.sql). Keep names single-word snake case if unavoidable (e.g.vectorindex). - Structure. Place DDL statements in dependency order: extensions → tables → indexes → constraints → seeds (if any).
- Transaction boundaries. The migration runner wraps each file in a transaction. Avoid statements that implicitly break transactions (e.g.
CREATE INDEX CONCURRENTLY). - Rollback notes. Add comments describing manual rollback steps when dropping columns or performing destructive operations.
- Testing. After authoring a migration, run
bun run db:migrateagainst a fresh database and ensure_migrationscount matches file count.
Schema Rules
- Single source. Define tables with Drizzle schema builders in
packages/db/src/schema. Keep column names aligned with migration SQL. - Timestamps. Use
timestamp("created_at", { withTimezone: true }).defaultNow()style helpers. Avoid relying on application clocks for created/updated fields. - Vector columns. Use the pgvector helper for embedding columns. Always document the expected dimensionality in comments.
- Foreign keys. Declare relationships explicitly so we can leverage Drizzle relations when needed. Name constraints
<table>_<column>_fkey. - Enums. Prefer Postgres enums defined in migrations and referenced via Drizzle
pgEnum. Avoid TypeScript-only enums for persisted values.
SQLite Fallback Schema
- SQLite-default UUIDs. SQLite schema must not use Postgres-only functions like
gen_random_uuid(); use SQLite-compatible defaults. - Default expressions. SQLite
DEFAULTexpressions that call functions must be wrapped in parentheses. - Normalization layer. Any SQLite schema normalizer must only rewrite known Postgres-only functions and must produce valid SQLite SQL.
- Drift-catcher test. Add a sqlite-only repo roundtrip test for new tables so schema errors fail immediately.