dot-skills Drizzle SQLite Best Practices
Library-reference skill for Drizzle ORM with SQLite-family backends. 45 rules across 8 categories, ordered by execution-lifecycle impact: schema → migrations → query → relations → transactions → performance → connection → types.
When to Apply
Reference these guidelines when:
- Defining
sqliteTableschemas — choosing column types, primary keys, indexes, foreign keys - Running
drizzle-kit generate/migrate/push, or hand-editing a migration SQL file - Writing queries with
db.select(),db.insert(),db.update(),db.delete() - Reaching for nested data with
db.query.*and the relational query builder - Wrapping multi-statement writes in
db.transaction()ordb.batch()(libsql/Turso/D1) - Optimizing a hot-path query with
.prepare()+sql.placeholder()or covering indexes - Setting up the Drizzle client (pragmas, driver choice, singleton lifecycle)
- Wiring database types into application code (
$inferSelect, drizzle-zod, JSON shapes)
The skill is not specific to one driver — it covers behavior shared across better-sqlite3, libsql, bun:sqlite, expo-sqlite, op-sqlite, and Cloudflare D1, calling out driver-specific deviations where they exist.
Architectural Context
SQLite is unusual among production databases:
- No client/server. The "connection" is a file open. There is no connection pool, no auth, no network in the local-file case.
- Single writer. One writer at a time, no matter how many connections. Reads can be parallel under WAL.
- No native booleans or dates. Everything is
INTEGER,REAL,TEXT,BLOB, orNULL— Drizzle column modes encode the rest. - Limited
ALTER TABLE. OnlyRENAME COLUMN,ADD COLUMN,DROP COLUMN. Type changes and constraint additions need a table rebuild. - Foreign keys off by default.
PRAGMA foreign_keys = ONis per-connection and not persistent.
Many rules in this skill exist because Drizzle's API abstracts over PostgreSQL/MySQL/SQLite uniformly — but the underlying SQLite engine has constraints that show up at runtime if you treat it like Postgres.
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Schema Definition | CRITICAL | schema- |
| 2 | Migrations & Drizzle Kit | CRITICAL | migrate- |
| 3 | Query Building | HIGH | query- |
| 4 | Relations | HIGH | rel- |
| 5 | Transactions & Batching | MEDIUM-HIGH | tx- |
| 6 | Prepared Statements & Hot Paths | MEDIUM-HIGH | perf- |
| 7 | Connection & Driver Setup | MEDIUM | conn- |
| 8 | Type Inference | MEDIUM | types- |
Quick Reference
1. Schema Definition (CRITICAL)
schema-integer-for-booleans— Useinteger({ mode: 'boolean' })so the inferred type isboolean, not0 | 1schema-timestamp-mode-for-dates— Store dates asinteger({ mode: 'timestamp_ms' }), not textschema-always-primary-key— Declare an explicit PK (single or composite); don't rely on hidden rowidschema-foreign-keys-with-actions— SpecifyonDelete/onUpdateon every.references()schema-index-foreign-keys-and-lookups— Index FK columns and frequentWHEREs — SQLite does not auto-index FKsschema-text-json-not-blob-json— Usetext({ mode: 'json' })sojson_extractand JSON-path indexes workschema-unique-constraints-for-natural-keys—.unique()for email/slug/externalId so onConflict has a target
2. Migrations & Drizzle Kit (CRITICAL)
migrate-generate-not-push-in-prod— Usegenerate + migrate;pushdrops columns it can't reconcilemigrate-explicit-renames— Answer the rename prompt — defaults treat renames as drop+addmigrate-config-dialect-and-out— Definedrizzle.config.tsso commands work without flagsmigrate-apply-with-migrator— Apply viadrizzle-kit migrateor the drivermigratormodule, not raw SQLmigrate-data-backfill-as-custom-sql— Hand-edit migration SQL to backfill atomically with the DDLmigrate-commit-migrations-to-git— Commitdrizzle/SQL anddrizzle/meta/snapshots — both are required
3. Query Building (HIGH)
query-select-columns-not-star— Project to the columns you need withdb.select({ ... })query-avoid-n-plus-one-with-inarray— Replace looped queries withinArray()query-always-limit-listings— Every listing query needs.limit()(and ideally a cursor)query-bind-parameters-not-concat— Useeq()/ sql template — never string-concat valuesquery-upsert-with-onconflict— Atomic upserts via.onConflictDoUpdate(), not select-then-writequery-returning-instead-of-reselect—.returning()on insert/update/delete saves a round tripquery-toSQL-and-explain— Inspect generated SQL andEXPLAIN QUERY PLANon hot paths
4. Relations (HIGH)
rel-declare-relations-for-rqb—relations()declarations unlockdb.query.*andwithrel-prefer-with-over-manual-joins—withfor nested fetches; manual joins lose typing and add coderel-partial-columns-in-with—columns: { ... }insidewithto limit payload and avoid leaksrel-filter-with-where-inside-with— Push related-row filters intowith.where, not into JSrel-leftjoin-for-flat-aggregates— Drop toleftJoin+groupBywhen you need aggregates
5. Transactions & Batching (MEDIUM-HIGH)
tx-wrap-multi-statement-writes— Wrap related writes indb.transaction()for atomicity + throughputtx-batch-for-libsql-roundtrips—db.batch()on libsql/Turso/D1 collapses N round trips into 1tx-no-network-io-inside-transaction— No awaited HTTP / FS / Stripe calls inside a transactiontx-handle-busy-with-retry— Bounded retries onSQLITE_BUSY— only on transient errorstx-single-writer-no-parallel-writes—Promise.allof writes contends; serialize them
6. Prepared Statements & Hot Paths (MEDIUM-HIGH)
perf-prepare-hot-paths—.prepare()+sql.placeholder()for queries running on every requestperf-bulk-insert-multi-row-values— Onevalues([...rows])instead of N looped insertsperf-avoid-count-star-on-large-tables— Counter rows or keyset pagination instead ofcount(*)perf-keyset-not-offset-for-deep-pages— Keyset pagination keeps cost constant across pagesperf-covering-index-for-hot-queries— Cover the projected columns so the planner skips the row read
7. Connection & Driver Setup (MEDIUM)
conn-enable-wal—journal_mode = WALfor concurrent reads + one writerconn-set-busy-timeout—busy_timeout = 5000turns contention into a waitconn-foreign-keys-pragma—foreign_keys = ONper connection — off by defaultconn-singleton-client— Module-scope singleton; never per-request constructionconn-pick-driver-deliberately— Sync vs async vs HTTP — choose by deployment target
8. Type Inference (MEDIUM)
types-infer-select-insert— Derive row types with$inferSelect/$inferInserttypes-narrow-json-with-dollartype—.$type<Shape>()to escapeunknownon JSON columnstypes-getTableColumns-for-reuse— Share projections viagetTableColumns()+ spreadtypes-drizzle-zod-for-runtime-validation—createInsertSchema(table)derives a Zod validator from the schematypes-bigint-mode-for-large-integers—mode: 'bigint'for IDs overNumber.MAX_SAFE_INTEGER
How to Use
Read the relevant category overview in references/_sections.md, then the specific rule files for detailed explanations and code examples. Each rule has incorrect-vs-correct examples — apply the correct pattern to the code under review.
For complex changes (schema redesign, migration strategy, performance work), read all rules in the affected category before deciding.
- Section definitions — Category structure and impact levels
- Rule template — Template for adding new rules
Reference Files
| File | Description | |------|-------------| | references/_sections.md | Category definitions and ordering | | assets/templates/_template.md | Template for new rules | | metadata.json | Version and reference information |
Related Skills
effect-ts— When the application is Effect-based; Drizzle integrates viaEffect.tryPromise.nextjs-bundle-optimizer— For Next.js apps reaching for SQLite as the data layer.better-auth— Often paired with Drizzle SQLite for auth tables; seebetter-auth-scaffoldfor table generation.