From f884891862f3b814076996c39bb1113e9577d61c Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Tue, 21 Apr 2026 20:31:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(blocks):=205=20database=20blocks=20?= =?UTF-8?q?=E2=80=94=20postgres/sqlite/sqlx/drizzle/migration-hygiene?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - db-postgres.md: PG17 patterns (indexes, pooling, backup); [E4] - db-sqlite.md: WAL prod patterns, Turso/LiteFS/D1, FTS5 - db-sqlx.md: Rust compile-time checked queries, offline mode - db-drizzle.md: TS schema-first, drizzle-kit migrations - db-migration-hygiene.md: universal up/down, zero-downtime, backfill, checksum tracking All blocks <60 LOC per Constructor Pattern. Version numbers marked [UNVERIFIED] where exact minor pins are needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- _blocks/db-drizzle.md | 51 +++++++++++++++++++++++++++++++++ _blocks/db-migration-hygiene.md | 33 +++++++++++++++++++++ _blocks/db-postgres.md | 28 ++++++++++++++++++ _blocks/db-sqlite.md | 34 ++++++++++++++++++++++ _blocks/db-sqlx.md | 39 +++++++++++++++++++++++++ 5 files changed, 185 insertions(+) create mode 100644 _blocks/db-drizzle.md create mode 100644 _blocks/db-migration-hygiene.md create mode 100644 _blocks/db-postgres.md create mode 100644 _blocks/db-sqlite.md create mode 100644 _blocks/db-sqlx.md diff --git a/_blocks/db-drizzle.md b/_blocks/db-drizzle.md new file mode 100644 index 0000000..daa8d2d --- /dev/null +++ b/_blocks/db-drizzle.md @@ -0,0 +1,51 @@ +# DB — Drizzle ORM (TypeScript) patterns + +Use when the project is TypeScript/Next.js/Bun/Node and needs a type-safe SQL layer without Prisma's heavyweight engine process. Pairs with `stack-nextjs`. [E4 — expert assessment] + +**Core versions:** `drizzle-orm` (latest on npm) + `drizzle-kit` (migrations CLI) as of 2026-04. Peer-deps: `pg` for Postgres, `better-sqlite3` / `@libsql/client` for SQLite, `mysql2` for MySQL. [UNVERIFIED: pin exact versions from npm before shipping] + +**Schema-first, not code-first:** +```ts +// db/schema.ts +import { pgTable, serial, text, timestamp, integer } from "drizzle-orm/pg-core"; + +export const users = pgTable("users", { + id: serial("id").primaryKey(), + email: text("email").notNull().unique(), + createdAt: timestamp("created_at").defaultNow().notNull(), +}); + +export const posts = pgTable("posts", { + id: serial("id").primaryKey(), + authorId: integer("author_id").references(() => users.id).notNull(), + body: text("body").notNull(), +}); +``` +`schema.ts` IS the source of truth. All types flow from it — `typeof users.$inferSelect` gives you the row type. + +**Query with full inference:** +```ts +import { eq } from "drizzle-orm"; +const rows = await db.select().from(users).where(eq(users.id, 1)); +// rows: { id: number; email: string; createdAt: Date }[] +``` +No codegen step, no separate `.prisma` file. Type errors surface in the IDE immediately. + +**Migrations via drizzle-kit:** +```bash +drizzle-kit generate # diff schema.ts against prev snapshot → emit SQL in drizzle/ +drizzle-kit migrate # apply pending migrations +drizzle-kit studio # local web UI to inspect data +``` +Config in `drizzle.config.ts` — specify `dialect`, `schema`, `out`, `dbCredentials`. + +**Connection / pool:** +```ts +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 20 }); +export const db = drizzle(pool, { schema }); +``` +Serverless (Vercel / CF Workers): use `neon-serverless` or `@libsql/client` driver instead — the `pg` Pool doesn't survive cold-start boundaries. + +**Forbidden:** template-string SQL with untrusted input (`sql\`SELECT * WHERE x = ${userInput}\`` — use `sql.placeholder` or the query builder); committing `drizzle/meta/_journal.json` conflicts (merge manually or regenerate); mixing drizzle-kit versions across dev machines. diff --git a/_blocks/db-migration-hygiene.md b/_blocks/db-migration-hygiene.md new file mode 100644 index 0000000..0532917 --- /dev/null +++ b/_blocks/db-migration-hygiene.md @@ -0,0 +1,33 @@ +# DB — Migration hygiene (universal) + +Applies to every migration tool — `kei-migrate`, Atlas, goose, sqlx-cli, drizzle-kit, Alembic, Prisma migrate, Ecto migrations. [E4 — expert assessment] + +**Numbering:** timestamp prefix, not integer. `20260421_120000_add_users_email_index.sql` sorts correctly forever and doesn't collide on parallel branches. Integer sequences (`0001_`, `0002_`) collide on merge; reject them in review. + +**Up + down pairs:** every migration has a reverse. If the reverse is destructive and unsafe (e.g. dropping a column with data), write a `-- IRREVERSIBLE` comment and stop the down-script there. NEVER auto-run destructive downs on prod without a human click. + +**Idempotent where possible:** +```sql +CREATE TABLE IF NOT EXISTS users (...); +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +ALTER TABLE users ADD COLUMN IF NOT EXISTS bio TEXT; -- PG 9.6+, verify per-DB +``` +Re-running a partially-applied migration should be safe. A migration that crashes mid-way and can't be re-run = 2AM incident waiting to happen. + +**Zero-downtime pattern (add-then-drop):** +1. Deploy migration that ADDS new column / table (old code still works). +2. Deploy app code that writes BOTH old + new. +3. Backfill old → new. +4. Deploy app code that reads new, ignores old. +5. Deploy migration that DROPS old column. + +Never `DROP` + `ADD RENAME` in one migration on a live table. That's a table lock + app-downtime event. + +**Backfill patterns:** +- Small table (< 1M rows): `UPDATE ... SET new = f(old)` in a single migration. +- Large table: background job with batched `UPDATE ... WHERE id BETWEEN ? AND ?` + `LIMIT`. Commit per batch. Monitor lag. +- Very large (> 100M rows): use the DB's native tooling (PG `VACUUM FULL` not needed; `pg_repack` if column-add bloats). [UNVERIFIED: verify on current PG docs] + +**Tracking table (`_kei_migrations` or equivalent):** stores (version, name, checksum, applied_at). Checksum prevents silent tampering with an already-applied file. If checksum mismatches on an applied migration → hard-fail, demand human intervention. + +**Forbidden:** editing a migration file after it's been applied on any environment (checksum break); `DROP TABLE` without backup + 24h cooldown; mixing DDL + large DML in one transaction (long locks); running migrations automatically on app startup in multi-replica deploys without a leader-election guard (every replica tries to apply = race condition). diff --git a/_blocks/db-postgres.md b/_blocks/db-postgres.md new file mode 100644 index 0000000..0997a80 --- /dev/null +++ b/_blocks/db-postgres.md @@ -0,0 +1,28 @@ +# DB — PostgreSQL (current major — 17 as of 2026-04) patterns + +Use when the project needs relational integrity, concurrent writes, or server-side indexing power that SQLite can't match. Default RDBMS for new multi-user services. [E4 — expert assessment] + +**Version choice:** PostgreSQL 17 for new projects (current GA line, improved vacuum, JSON_TABLE, better parallel index builds). PostgreSQL 16 acceptable if hosting provider pins it. [UNVERIFIED: exact feature matrix — verify on postgresql.org/docs before committing to a minor-version-specific feature] + +**Schema migrations:** every schema change ships as a numbered `.sql` file, never `ALTER TABLE` on prod. Use `kei-migrate` (this kit) or Atlas/goose/sqlx-cli — see `db-migration-hygiene.md`. One migration per logical change; no mega-migrations. + +**Indexing:** +- B-tree default for equality + range. `CREATE INDEX CONCURRENTLY` on prod to avoid table lock. +- `GIN` for `jsonb` / array / full-text (`tsvector`). +- `BRIN` only for massive append-only time-series (orders of magnitude smaller than B-tree). +- Partial indexes (`WHERE active = true`) for sparse predicates. +- **Verify with `EXPLAIN (ANALYZE, BUFFERS)`** before declaring an index necessary. No blind indexing. + +**Connection pooling:** app-side connection pool is NOT enough at scale. Use: +- **PgBouncer** (transaction mode) for most services — battle-tested, low overhead. +- **Supavisor** if already on Supabase — serverless-friendly, wire-compatible. [E4] +- Native server pooling (PG 17's improved but still not a substitute). [UNVERIFIED] + +Sizing rule of thumb: `max_connections` on server × 1 pool layer. Don't stack pools (pool → PgBouncer → PG = deadlock risk). + +**Backup:** +- Logical: `pg_dump` nightly for schema + data portability. +- Physical: `pg_basebackup` + WAL archiving (`archive_command`) for PITR. +- Managed service (RDS / Supabase / Neon) — verify backup retention in their UI, don't assume. + +**Forbidden:** `SELECT *` in hot paths (N+1 + column drift); unindexed FK columns (join explosion); `SERIAL` on new tables — prefer `GENERATED ALWAYS AS IDENTITY` (SQL standard, PG 10+); plaintext passwords in `pg_hba.conf`; committing `.env` with DB URL. diff --git a/_blocks/db-sqlite.md b/_blocks/db-sqlite.md new file mode 100644 index 0000000..65399cc --- /dev/null +++ b/_blocks/db-sqlite.md @@ -0,0 +1,34 @@ +# DB — SQLite (prod-suitable) patterns + +Use when the workload is read-heavy, single-writer-acceptable, or needs zero-ops embedded storage. SQLite is prod-suitable — Fly.io, Turso, Cloudflare D1, and countless CLI/mobile apps run it in production. [E4 — expert assessment] + +**When NOT to use:** high-concurrency write workload (> ~1 writer/sec sustained), multi-region strong consistency, horizontal write scaling. Use Postgres instead. + +**WAL mode is mandatory for prod:** +```sql +PRAGMA journal_mode = WAL; -- readers don't block writer, writer doesn't block readers +PRAGMA synchronous = NORMAL; -- durable across app crash, NOT across power loss (use FULL if PSU-risk) +PRAGMA busy_timeout = 5000; -- 5s wait for lock instead of instant SQLITE_BUSY +PRAGMA foreign_keys = ON; -- default OFF in SQLite (!), always enable +PRAGMA temp_store = MEMORY; +``` +Apply these on every connection open — they are per-connection, not per-database (except `journal_mode` which persists). + +**Distributed patterns:** +- **Turso** (libSQL fork): edge-replicated read replicas with HTTP/WebSocket wire protocol. Primary single-writer, replicas read-only. [E4] +- **LiteFS** (Fly.io): file-system replication, leader-election via Consul. Primary+replicas. [E4] +- **Cloudflare D1**: managed SQLite on edge with their own replication. [UNVERIFIED: current throughput limits] +- **Litestream**: continuous replication to S3/R2 for backup + PITR; single node, not HA. + +**Full-text search (FTS5):** +```sql +CREATE VIRTUAL TABLE docs_fts USING fts5(title, body, content=docs, content_rowid=id); +CREATE TRIGGER docs_ai AFTER INSERT ON docs BEGIN + INSERT INTO docs_fts(rowid, title, body) VALUES (new.id, new.title, new.body); +END; +``` +FTS5 outperforms bolt-on `LIKE '%x%'` by 100×+ on large text corpora. Native, no extension install. + +**Backup:** `sqlite3 db '.backup /path/backup.db'` while app runs (safe with WAL). Or Litestream for continuous. + +**Forbidden:** multiple writer processes without a coordination layer; opening the same DB over NFS (lock semantics broken); `DELETE FROM bigtable` without `VACUUM` after (doesn't shrink file); committing the `.db` / `.db-wal` / `.db-shm` files to git. diff --git a/_blocks/db-sqlx.md b/_blocks/db-sqlx.md new file mode 100644 index 0000000..54ada28 --- /dev/null +++ b/_blocks/db-sqlx.md @@ -0,0 +1,39 @@ +# DB — SQLx (Rust) patterns + +Use when the project is Rust and needs a SQL-first (not ORM) query layer with compile-time checking. Pairs with `stack-rust-axum`, `stack-rust-cli`. [E4 — expert assessment] + +**Core versions:** `sqlx = "0.8"` (current as of 2026-04) with features `runtime-tokio`, `tls-rustls`, and one of `postgres` / `sqlite` / `mysql`. Never mix `runtime-async-std` and `runtime-tokio` — they clash at link time. [UNVERIFIED: verify latest on crates.io before pinning] + +**Compile-time checked queries:** +```rust +let row = sqlx::query!("SELECT id, name FROM users WHERE id = $1", user_id) + .fetch_one(&pool).await?; +``` +Requires either: +- `DATABASE_URL` env set during `cargo build` (live DB) — convenient in dev, brittle in CI. +- **Offline mode** (recommended for CI): `cargo sqlx prepare` commits `.sqlx/query-*.json` to the repo, then CI builds with `SQLX_OFFLINE=true` and no DB access. + +**Connection pool:** +```rust +let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(20) // tune to server max_connections / replica count + .acquire_timeout(Duration::from_secs(3)) + .connect(&database_url).await?; +``` +Single `PgPool` per process, `Arc`-cloned into handlers. Don't open per-request. + +**Migrations:** +```rust +sqlx::migrate!("./migrations").run(&pool).await?; +``` +Built-in runner reads `YYYYMMDDHHMMSS_.sql` files. For richer UX (up/down, status, create scaffolding) use the `kei-migrate` primitive in this kit. + +**Transactions:** +```rust +let mut tx = pool.begin().await?; +sqlx::query!("...").execute(&mut *tx).await?; +sqlx::query!("...").execute(&mut *tx).await?; +tx.commit().await?; // explicit; Drop = rollback +``` + +**Forbidden:** `sqlx::query` (non-macro) with untrusted input without `bind()` — that's string concat, i.e. SQL injection; `.unwrap()` on DB calls in prod paths; enabling both `runtime-tokio` and `runtime-async-std`; committing a live `DATABASE_URL` to `.env.example`.