--- name: database-orm-expert description: "Updated to be the unified database skill covering ORM, migrations, edge DBs, and Supabase CLI / Keahlian database terpadu untuk ORM, migrasi, edge DB, dan Supabase CLI." author: "Roedy Rustam" version: "4.4.2" --- # Unified Database & ORM Expert (2026 Production Edition) [English](#english) | [Bahasa Indonesia](#bahasa-indonesia) --- ## English ### Orchestration & Integration Connects and orchestrates with domain skills: - **Backend & Runtimes**: `js-backend-expert`, `python-programming-expert`, `go-programming-expert`, and `rust-programming-expert` for integrating Drizzle ORM, Prisma 6, SQLx, and SQLAlchemy/SQLModel. - **FinTech & SaaS**: `saas-billing`, `saas-multi-tenant`, `double-entry-ledger-fintech-expert`, and `doku-payment-gateway` for strict atomic database transactions (`SELECT ... FOR UPDATE`), distributed locks, tenant isolation via RLS, and SNAP BI idempotent records. - **AI & Vectors**: `vector-db-rag-expert` for pgvector 0.8+ HNSW indexing and high-dimensional cosine similarity embeddings. - **Security & Cloud**: `supabase-security-expert`, `fga-rebac-authorization-expert`, `ci-cd-devops-architect`, and `cloud-hosting-expert` for automated migration pipelines, blue-green deployments, and pooler infrastructure. - **Master Orchestrators**: `brainstorming` and `zero-to-prod-orchestrator` (Phase 3: Schema & Database Architecture). ### Description Production-grade guide for relational schema design, ORM selection, zero-downtime Expand-and-Contract migrations, pessimistic concurrency locking, query optimization, and edge serverless database drivers. Covers **Drizzle ORM v0.39+**, **Prisma 6 (TypedSQL & Relation Joins)**, **Supabase CLI**, and Edge Databases (**Neon Serverless Postgres**, **Cloudflare D1**, **Turso libSQL**, **Upstash Redis**). ### Trigger Conditions - Designing or migrating a production schema (blue-green, Expand-and-Contract). - Choosing between Drizzle ORM, Prisma 6, Kysely, or SQLx. - Writing complex queries with joins, cursor pagination, or atomic row locks. - Optimizing slow queries, missing FK indexes, or N+1 problems. - Building edge-compatible serverless database connections (HTTP/WebSocket poolers). - Executing Supabase CLI migrations and Row-Level Security (RLS) policies. --- ### Core ORM & Query Strategies #### ORM Selection Matrix (2026) | Criteria | Drizzle ORM (v0.39+) | Prisma 6 | Kysely | | :--- | :--- | :--- | :--- | | **Type Safety** | SQL-first, zero-codegen TypeScript inference | Schema-first (`schema.prisma`) + TypedSQL | Pure type-safe SQL query builder | | **Cold Start & Bundle** | Ultra-lightweight (<35KB, zero binary) | Wasm/Rust query compiler (~1.5MB) | Minimal (<20KB) | | **Relational Queries** | `db.query.table.findMany({ with })` (Single SQL join) | `relationLoadStrategy: "join"` | Explicit `.innerJoin()` | | **Edge Runtime** | Native (Cloudflare Workers, Vercel Edge, Bun) | Native via `@prisma/adapter-neon` / D1 | Native | | **Migrations** | `drizzle-kit generate` & `drizzle-kit migrate` | `prisma migrate dev` / `deploy` | Custom migration runner | **Recommendation**: Prefer **Drizzle ORM** for edge-first apps, Hono/Bun backends, and performance-critical systems. Choose **Prisma 6** for teams preferring declarative `.prisma` modeling and TypedSQL. --- #### 1. Drizzle ORM v0.39+ Production Schema & Atomic Locking Always index foreign keys explicitly, enforce `TIMESTAMPTZ`, and use `FOR UPDATE` row locks on financial or inventory mutations. ```typescript // src/db/schema.ts import { pgTable, uuid, varchar, bigint, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core'; import { relations, sql } from 'drizzle-orm'; export const tenants = pgTable('tenants', { id: uuid('id').defaultRandom().primaryKey(), slug: varchar('slug', { length: 64 }).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), }, (t) => [ uniqueIndex('idx_tenants_slug').on(t.slug), ]); export const wallets = pgTable('wallets', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull().references(() => tenants.id, { onDelete: 'restrict' }), currency: varchar('currency', { length: 3 }).notNull().default('IDR'), balanceMinor: bigint('balance_minor', { mode: 'bigint' }).notNull().default(0n), version: bigint('version', { mode: 'number' }).notNull().default(1), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), }, (t) => [ index('idx_wallets_tenant_id').on(t.tenantId), ]); export const paymentIdempotency = pgTable('payment_idempotency', { idempotencyKey: varchar('idempotency_key', { length: 128 }).primaryKey(), walletId: uuid('wallet_id').notNull().references(() => wallets.id, { onDelete: 'cascade' }), amountMinor: bigint('amount_minor', { mode: 'bigint' }).notNull(), status: varchar('status', { length: 24 }).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), }, (t) => [ index('idx_payment_idempotency_wallet').on(t.walletId), ]); export const walletsRelations = relations(wallets, ({ one }) => ({ tenant: one(tenants, { fields: [wallets.tenantId], references: [tenants.id] }), })); ``` ```typescript // src/db/mutations/debit-wallet.ts import { eq, sql } from 'drizzle-orm'; import { db } from '../client'; import { wallets, paymentIdempotency } from '../schema'; export async function debitWalletAtomic(input: { walletId: string; amountMinor: bigint; idempotencyKey: string; }) { if (input.amountMinor <= 0n) { throw new Error('Debit amount must be strictly positive'); } return await db.transaction(async (tx) => { // 1. Check idempotency key first const [existing] = await tx .select() .from(paymentIdempotency) .where(eq(paymentIdempotency.idempotencyKey, input.idempotencyKey)) .limit(1); if (existing) { return { status: 'ALREADY_PROCESSED' as const, record: existing }; } // 2. Acquire pessimistic row-level lock (SELECT ... FOR UPDATE) const [wallet] = await tx .select() .from(wallets) .where(eq(wallets.id, input.walletId)) .for('update'); if (!wallet) { throw new Error(`Wallet ${input.walletId} not found`); } if (wallet.balanceMinor < input.amountMinor) { throw new Error('Insufficient wallet balance'); } // 3. Deduct balance and increment optimistic version const [updatedWallet] = await tx .update(wallets) .set({ balanceMinor: sql`${wallets.balanceMinor} - ${input.amountMinor}`, version: sql`${wallets.version} + 1`, updatedAt: new Date(), }) .where(eq(wallets.id, input.walletId)) .returning(); // 4. Record idempotency lock in same transaction const [record] = await tx .insert(paymentIdempotency) .values({ idempotencyKey: input.idempotencyKey, walletId: input.walletId, amountMinor: input.amountMinor, status: 'COMPLETED', }) .returning(); return { status: 'COMPLETED' as const, wallet: updatedWallet, record }; }, { isolationLevel: 'read committed', }); } ``` --- #### 2. Keyset (Cursor) Pagination Pattern Never use `OFFSET` on tables exceeding 50,000 rows (`OFFSET 100000` forces Postgres to scan and discard 100,000 rows). Always use composite `(createdAt, id)` keyset cursors: ```typescript import { and, desc, eq, lt, or } from 'drizzle-orm'; import { db } from './client'; import { wallets } from './schema'; export async function listWalletsCursor(params: { tenantId: string; limit?: number; cursor?: { updatedAt: Date; id: string }; }) { const limit = Math.min(params.limit ?? 25, 100); const cursorFilter = params.cursor ? or( lt(wallets.updatedAt, params.cursor.updatedAt), and(eq(wallets.updatedAt, params.cursor.updatedAt), lt(wallets.id, params.cursor.id)) ) : undefined; const rows = await db .select() .from(wallets) .where(and(eq(wallets.tenantId, params.tenantId), cursorFilter)) .orderBy(desc(wallets.updatedAt), desc(wallets.id)) .limit(limit + 1); const hasNextPage = rows.length > limit; const items = hasNextPage ? rows.slice(0, limit) : rows; const lastItem = items[items.length - 1]; return { items, nextCursor: hasNextPage && lastItem ? { updatedAt: lastItem.updatedAt, id: lastItem.id } : null, }; } ``` --- ### Zero-Downtime Migrations (Expand and Contract Protocol) Never execute breaking DDL changes (`DROP COLUMN`, `RENAME COLUMN`, or locking `ALTER TABLE ... ADD COLUMN ... NOT NULL` without default) in a single release while old application containers are still serving traffic. 1. **Phase 1 (Expand)**: Add nullable column or new table; create indexes using `CREATE INDEX CONCURRENTLY` to avoid blocking writes. 2. **Phase 2 (Dual-Write)**: Update application code to write to both old and new columns; read from old with fallback. 3. **Phase 3 (Chunked Backfill)**: Backfill historical rows in small batches (`LIMIT 2000`) with `FOR UPDATE SKIP LOCKED` to avoid replication lag and table locks. 4. **Phase 4 (Cutover Read & Contract)**: Switch reads to the new column, remove dual-write code, and validate constraints using `NOT VALID` followed by `VALIDATE CONSTRAINT`. ```sql -- Safe Zero-Lock Constraint & Concurrent Index Migration ALTER TABLE wallets ADD COLUMN IF NOT EXISTS ledger_account_id UUID; CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_wallets_ledger_account_id ON wallets (ledger_account_id); -- Add foreign key without locking existing writes (Postgres 14+) ALTER TABLE wallets ADD CONSTRAINT fk_wallets_ledger_account FOREIGN KEY (ledger_account_id) REFERENCES accounts(id) NOT VALID; -- Validate constraint using ShareUpdateExclusiveLock (does not block reads/writes) ALTER TABLE wallets VALIDATE CONSTRAINT fk_wallets_ledger_account; ``` --- ### Edge & Serverless Drivers + Cache-Aside ```typescript import { neon } from '@neondatabase/serverless'; import { drizzle } from 'drizzle-orm/neon-http'; import { Redis } from '@upstash/redis'; import * as schema from './schema'; const sqlClient = neon(process.env.DATABASE_URL!); export const edgeDb = drizzle(sqlClient, { schema }); const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, }); export async function getTenantCached(slug: string) { const cacheKey = `tenant:v1:${slug}`; const cached = await redis.get(cacheKey); if (cached) return cached; const tenant = await edgeDb.query.tenants.findFirst({ where: (t, { eq }) => eq(t.slug, slug), }); if (tenant) { await redis.set(cacheKey, tenant, { ex: 900 }); } return tenant ?? null; } ``` --- ### Verification Checklist - [ ] Every foreign key column has a corresponding B-Tree index (`idx__`). - [ ] Financial balances and monetary quantities use `BIGINT` minor units (cents/sen) or `NUMERIC(19,4)`, never `FLOAT`/`REAL`. - [ ] Multi-tenant tables enforce Row-Level Security (`ALTER TABLE ... ENABLE ROW LEVEL SECURITY`) and tenant ID predicates. - [ ] Production migrations never run `prisma db push` or `drizzle-kit push`; versioned SQL files are audited via `vibes db:eval`. - [ ] High-concurrency state transitions use `SELECT ... FOR UPDATE` or optimistic version increments. ### Sovereign Execution, Token Efficiency & Invariants | Dimension | Sovereign Mandate & Deterministic Contract | | :--- | :--- | | **Frontier Model Cascade** | Medium / High (`Gemini 4 Argon`, `GPT-5.6 Sol Medium`, `Claude Sonnet 5.5 High`) | | **Availability Fallback** | `Gemini 4 Argon/Pro` -> `Gemini 3.8 Flash`; `Claude Opus 5.5` -> `Claude Opus 4.6` (Zero unhandled missing-model errors) | | **KV-Cache & Token Protocol** | Pin static instructions & schemas at prompt prefix (`kv-cache-prefix-optimizer`); pass surgical `CONTEXT_MAP.md` slices (<250 tokens/call) during Swarm Fan-Out | | **SaaT CLI & MCP Dispatch** | CLI: `vibes skill exec database-orm-expert ` / MCP: `vibes_skill_dispatch`, `vibes_eval_sql`, `vibes_verify_invariants`, `vibes_verify_gate` | | **NFR & Anti-Slop Invariants** | P99 Query Latency < 15ms, 100% Row Level Security (RLS) coverage on multi-tenant tables, explicit B-Tree indexes on all Foreign Keys; 100% complete zero-placeholder output verified via `node scripts/check-anti-slop.js --strict` | --- --- ## Bahasa Indonesia ### Integrasi Orkestrasi Terhubung dan mengorkestrasi bersama skill domain: - **Backend & Runtime**: `js-backend-expert`, `python-programming-expert`, `go-programming-expert`, dan `rust-programming-expert` untuk integrasi Drizzle ORM, Prisma 6, SQLx, dan SQLAlchemy/SQLModel. - **FinTech & SaaS**: `saas-billing`, `saas-multi-tenant`, `double-entry-ledger-fintech-expert`, dan `doku-payment-gateway` untuk transaksi database atomik yang ketat (`SELECT ... FOR UPDATE`), distributed lock, isolasi tenant via RLS, dan pencatatan idempoten SNAP BI. - **AI & Vektor**: `vector-db-rag-expert` untuk pengindeksan pgvector 0.8+ HNSW dan pencarian embedding kesamaan kosinus dimensi tinggi. - **Keamanan & Cloud**: `supabase-security-expert`, `fga-rebac-authorization-expert`, `ci-cd-devops-architect`, dan `cloud-hosting-expert` untuk pipeline deployment migrasi otomatis, blue-green, dan infrastruktur connection pooler. - **Orkestrator Utama**: `brainstorming` dan `zero-to-prod-orchestrator` (Fase 3: Arsitektur Skema & Database). ### Deskripsi Panduan kelas produksi untuk perancangan skema relasional, pemilihan ORM, migrasi tanpa downtime (*Expand-and-Contract*), penguncian konkurensi pesimistis, optimasi query, dan driver database serverless edge (**Drizzle ORM v0.39+**, **Prisma 6**, **Supabase CLI**, **Neon**, **Cloudflare D1**, **Turso**, **Upstash**). ### Strategi Inti ORM & Query 1. **Drizzle ORM** direkomendasikan untuk aplikasi edge dan performa tinggi (<35KB, tanpa biner), sedangkan **Prisma 6** untuk alur deklaratif `schema.prisma` dan TypedSQL. 2. **Indeks Foreign Key**: Selalu buat indeks eksplisit pada setiap kolom foreign key dan kolom filter `WHERE` / `ORDER BY`. 3. **Keyset (Cursor) Pagination**: Gunakan kursor komposit `(updated_at, id)` dan hindari `OFFSET` pada tabel skala besar. 4. **Transaksi & Kunci Baris Atomik**: Gunakan `SELECT ... FOR UPDATE` di dalam transaksi database untuk mencegah kondisi balapan (*race condition*) pada pemotongan saldo atau stok. ### Migrasi Tanpa Downtime (Expand and Contract) - **Fase 1 (Expand)**: Tambahkan kolom baru bersifat *nullable* dan gunakan `CREATE INDEX CONCURRENTLY`. - **Fase 2 (Dual-Write)**: Tulis ke kolom lama dan baru secara bersamaan. - **Fase 3 (Chunked Backfill)**: Lakukan *backfill* data secara bertahap (`LIMIT 2000`) menggunakan antrean latar belakang. - **Fase 4 (Contract)**: Pindahkan pembacaan ke kolom baru, validasi *constraint* dengan `NOT VALID` + `VALIDATE CONSTRAINT`, lalu hapus kode lama. ### Daftar Periksa Verifikasi (Checklist) - [ ] Setiap kolom foreign key memiliki indeks B-Tree eksplisit. - [ ] Nilai uang/saldo menggunakan `BIGINT` satuan terkecil (sen/rupiah utuh) atau `NUMERIC(19,4)`, bukan `FLOAT`. - [ ] Tabel multi-tenant mengaktifkan Row-Level Security (RLS). - [ ] Migrasi produksi menggunakan file SQL terversi dan diuji dengan `vibes db:eval`. ### Matriks Eksekusi Berdaulat, Efisiensi Token & Invarian | Dimensi | Mandat Berdaulat & Kontrak Deterministik | | :--- | :--- | | **Kaskade Model Frontier** | Medium / High (`Gemini 4 Argon`, `GPT-5.6 Sol Medium`, `Claude Sonnet 5.5 High`) | | **Invarian Fallback Ketersediaan** | `Gemini 4 Argon/Pro` -> `Gemini 3.8 Flash`; `Claude Opus 5.5` -> `Claude Opus 4.6` (Bebas gagal saat model belum tersedia/kuota penuh) | | **Protokol Hemat Token & KV-Cache** | Kunci instruksi & skema statis di prefix awal (`kv-cache-prefix-optimizer`); kirim potongan `CONTEXT_MAP.md` (<250 token/panggilan) saat Fan-Out | | **Eksekusi SaaT CLI & MCP** | CLI: `vibes skill exec database-orm-expert ` / MCP: `vibes_skill_dispatch`, `vibes_eval_sql`, `vibes_verify_invariants`, `vibes_verify_gate` | | **Invarian NFR & Anti-Slop** | Latensi Query P99 < 15ms, cakupan Row Level Security (RLS) 100% pada tabel multi-tenant, indeks B-Tree eksplisit pada seluruh Foreign Key; kode 100% tuntas tanpa placeholder diverifikasi melalui `node scripts/check-anti-slop.js --strict` |