Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:43:11 +08:00
commit 5cf0559508
28 changed files with 5938 additions and 0 deletions

View File

@@ -0,0 +1,478 @@
# Adapter Reference Guide
Complete guide for choosing between HTTP and WebSocket adapters.
## Table of Contents
- [Quick Decision Matrix](#quick-decision-matrix)
- [HTTP Adapter](#http-adapter-neondatabaseserverless-with-neon-http)
- [WebSocket Adapter](#websocket-adapter-neondatabaseserverless-with-neon-serverless)
- [Framework-Specific Recommendations](#framework-specific-recommendations)
- [Mixed Environments](#mixed-environments)
- [Feature Comparison Table](#feature-comparison-table)
- [Performance Considerations](#performance-considerations)
- [Troubleshooting](#troubleshooting)
- [Migration Between Adapters](#migration-between-adapters)
- [Choosing the Right Adapter](#choosing-the-right-adapter)
- [Related Resources](#related-resources)
---
## Quick Decision Matrix
| Environment | Adapter | Reason |
|-------------|---------|--------|
| Vercel | HTTP | Edge functions, stateless |
| Cloudflare Workers | HTTP | Edge runtime, no WebSocket |
| AWS Lambda | HTTP | Stateless, cold starts |
| Next.js (Vercel) | HTTP | App Router, Edge Runtime |
| Express/Fastify | WebSocket | Long-lived connections |
| Node.js server | WebSocket | Connection pooling |
| Bun server | WebSocket | Persistent runtime |
## HTTP Adapter (@neondatabase/serverless with neon-http)
### When to Use
**Serverless/Edge environments:**
- Vercel Edge Functions
- Cloudflare Workers
- AWS Lambda
- Deno Deploy
- Next.js App Router (default)
**Characteristics:**
- Stateless requests
- Cold starts
- Short execution time
- No persistent connections
### Setup
**Installation:**
```bash
npm add drizzle-orm @neondatabase/serverless
npm add -D drizzle-kit
```
**Connection:**
```typescript
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);
```
**Complete example:** See `templates/db-http.ts`
### Pros
**Perfect for serverless:**
- No connection management needed
- Works in edge environments
- Fast cold starts
- Auto-scales
**Simple:**
- Minimal configuration
- No connection pooling complexity
- Stateless = predictable
### Cons
**Limited features:**
- No transactions
- No prepared statements
- No streaming
- Higher latency per query
**Not ideal for:**
- Batch operations
- Complex transactions
- High-frequency queries from same process
### Best Practices
**1. Use batch for multiple operations:**
```typescript
await db.batch([
db.insert(users).values({ email: 'test@example.com' }),
db.insert(posts).values({ title: 'Test' }),
]);
```
**2. Cache query results:**
```typescript
import { unstable_cache } from 'next/cache';
const getUsers = unstable_cache(
async () => db.select().from(users),
['users'],
{ revalidate: 60 }
);
```
**3. Minimize round trips:**
```typescript
const usersWithPosts = await db.query.users.findMany({
with: { posts: true },
});
```
## WebSocket Adapter (@neondatabase/serverless with neon-serverless)
### When to Use
**Long-lived processes:**
- Express/Fastify servers
- Standard Node.js applications
- Background workers
- WebSocket servers
- Bun applications
**Characteristics:**
- Persistent connections
- Long execution time
- Connection pooling
- Complex transactions
### Setup
**Installation:**
```bash
npm add drizzle-orm @neondatabase/serverless ws
npm add -D drizzle-kit @types/ws
```
**Connection:**
```typescript
import { drizzle } from 'drizzle-orm/neon-serverless';
import { Pool, neonConfig } from '@neondatabase/serverless';
import ws from 'ws';
neonConfig.webSocketConstructor = ws;
const pool = new Pool({
connectionString: process.env.DATABASE_URL!,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
export const db = drizzle(pool);
```
**Complete example:** See `templates/db-websocket.ts`
### Pros
**Full features:**
- Transactions
- Prepared statements
- Streaming
- Lower latency (persistent connection)
**Better for:**
- Multiple queries per request
- Complex business logic
- High-frequency operations
### Cons
**More complex:**
- Connection pool management
- Need to handle connection errors
- Not available in edge environments
**Resource considerations:**
- Connection limits
- Memory usage
- Cold start overhead
### Best Practices
**1. Configure connection pool:**
```typescript
const pool = new Pool({
connectionString: process.env.DATABASE_URL!,
max: 10, // Max connections
idleTimeoutMillis: 30000, // Close idle after 30s
connectionTimeoutMillis: 5000, // Timeout after 5s
});
```
**2. Graceful shutdown:**
```typescript
process.on('SIGTERM', async () => {
await pool.end();
process.exit(0);
});
process.on('SIGINT', async () => {
await pool.end();
process.exit(0);
});
```
**3. Use transactions:**
```typescript
await db.transaction(async (tx) => {
const user = await tx.insert(users)
.values({ email: 'test@example.com' })
.returning();
await tx.insert(posts)
.values({ userId: user[0].id, title: 'First post' });
});
```
**4. Handle connection errors:**
```typescript
pool.on('error', (err) => {
console.error('Unexpected pool error:', err);
});
pool.on('connect', () => {
console.log('Pool connection established');
});
```
## Framework-Specific Recommendations
### Next.js
**App Router (default):**
- Use HTTP adapter (Edge Runtime)
- Server Actions → HTTP
- Route Handlers → HTTP
**Pages Router:**
- API Routes → Either adapter works
- Recommend HTTP for consistency
**Example:**
```typescript
// app/actions/users.ts
'use server';
import { db } from '@/db'; // HTTP adapter
import { users } from '@/db/schema';
export async function createUser(email: string) {
return db.insert(users).values({ email }).returning();
}
```
### Express
**Standard setup:**
- Use WebSocket adapter
- Configure connection pool
- Implement health checks
**Example:**
```typescript
import express from 'express';
import { db } from './db'; // WebSocket adapter
import { users } from './db/schema';
const app = express();
app.get('/health', async (req, res) => {
try {
await db.select().from(users).limit(1);
res.json({ status: 'healthy' });
} catch (err) {
res.status(500).json({ status: 'unhealthy', error: err.message });
}
});
app.listen(3000);
```
### Vite/React (SPA)
**Deployment matters:**
**If deploying to Vercel:**
- API routes → HTTP adapter
- Static files → No backend needed
**If deploying to Node.js server:**
- Backend API → WebSocket adapter
- Frontend → Fetch from API
### Bun
**Recommendation:**
- Use WebSocket adapter
- Bun has built-in WebSocket support
- No need for `ws` package
**Setup:**
```typescript
import { drizzle } from 'drizzle-orm/neon-serverless';
import { Pool } from '@neondatabase/serverless';
const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
export const db = drizzle(pool);
```
## Mixed Environments
### Using Both Adapters
If you have both serverless and long-lived components:
**Structure:**
```
src/
├── db/
│ ├── http.ts # HTTP adapter for serverless
│ ├── ws.ts # WebSocket for servers
│ └── schema.ts # Shared schema
```
**HTTP adapter:**
```typescript
// src/db/http.ts
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
export const httpDb = drizzle(sql);
```
**WebSocket adapter:**
```typescript
// src/db/ws.ts
import { drizzle } from 'drizzle-orm/neon-serverless';
import { Pool, neonConfig } from '@neondatabase/serverless';
import ws from 'ws';
neonConfig.webSocketConstructor = ws;
const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
export const wsDb = drizzle(pool);
```
**Usage:**
```typescript
// Vercel Edge Function
import { httpDb as db } from '@/db/http';
// Express route
import { wsDb as db } from '@/db/ws';
```
## Feature Comparison Table
| Feature | HTTP Adapter | WebSocket Adapter |
|---------|-------------|-------------------|
| Transactions | ❌ No | ✅ Yes |
| Prepared statements | ❌ No | ✅ Yes |
| Streaming results | ❌ No | ✅ Yes |
| Connection pooling | N/A (stateless) | ✅ Yes |
| Edge runtime | ✅ Yes | ❌ No |
| Cold start speed | ✅ Fast | ⚠️ Slower |
| Latency per query | ⚠️ Higher | ✅ Lower |
| Batch operations | ✅ Yes | ✅ Yes |
| Max connection limit | N/A | ⚠️ Applies |
## Performance Considerations
### HTTP Adapter Performance
**Optimize by:**
- Minimizing round trips
- Using batch operations
- Caching query results
- Pre-fetching related data
**Typical latency:**
- Single query: 50-200ms
- Batch operation: 100-300ms
### WebSocket Adapter Performance
**Optimize by:**
- Configuring pool size correctly
- Using transactions for related operations
- Implementing query caching
- Monitoring connection usage
**Typical latency:**
- First query (connection): 50-100ms
- Subsequent queries: 10-50ms
## Troubleshooting
### HTTP Adapter Issues
**Problem:** "fetch is not defined"
- **Solution:** Ensure running in environment with fetch API (Node 18+, edge runtime)
**Problem:** Slow queries
- **Solution:** Use batch operations, reduce round trips
### WebSocket Adapter Issues
**Problem:** "WebSocket is not defined"
- **Solution:** Add `neonConfig.webSocketConstructor = ws`
**Problem:** "Too many connections"
- **Solution:** Reduce pool `max` size, ensure connections are closed
**Problem:** Connection timeouts
- **Solution:** Increase `connectionTimeoutMillis`, implement retry logic
## Migration Between Adapters
### HTTP → WebSocket
**When:** Moving from serverless to dedicated server.
**Steps:**
1. Install ws: `npm add ws @types/ws`
2. Update connection file to WebSocket adapter
3. Update drizzle.config.ts if needed
4. Test transactions (now available)
### WebSocket → HTTP
**When:** Moving to serverless/edge deployment.
**Steps:**
1. Update connection file to HTTP adapter
2. Remove ws dependency
3. **Important:** Replace transactions with batch operations
4. Test thoroughly (feature differences)
## Choosing the Right Adapter
**Ask yourself:**
1. **Where am I deploying?**
- Edge/Serverless → HTTP
- Node.js server → WebSocket
2. **Do I need transactions?**
- Yes → WebSocket
- No → Either works
3. **What's my request pattern?**
- Short, infrequent → HTTP
- Long, frequent → WebSocket
4. **Am I optimizing for?**
- Cold starts → HTTP
- Latency → WebSocket
**When in doubt:** Start with HTTP (simpler), migrate to WebSocket if needed.
## Related Resources
- `guides/new-project.md` - Setup guides for both adapters
- `guides/troubleshooting.md` - Connection error solutions
- `templates/db-http.ts` - HTTP adapter template
- `templates/db-websocket.ts` - WebSocket adapter template

View File

@@ -0,0 +1,652 @@
# Migration Reference Guide
Complete guide for database migrations with Drizzle and Neon.
## Table of Contents
- [Migration Lifecycle](#migration-lifecycle)
- [Environment Loading Deep-Dive](#environment-loading-deep-dive)
- [Migration Patterns](#migration-patterns)
- [Advanced Patterns](#advanced-patterns)
- [Migration in CI/CD](#migration-in-cicd)
- [Common Migration Errors](#common-migration-errors)
- [Best Practices](#best-practices)
- [Related Resources](#related-resources)
---
## Migration Lifecycle
### 1. Schema Change
Update your schema file:
```typescript
// src/db/schema.ts
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull(),
phoneNumber: varchar('phone_number', { length: 20 }), // NEW
});
```
### 2. Generate Migration
Run drizzle-kit to generate SQL:
```bash
npm run drizzle-kit generate
```
**What this does:**
- Compares schema.ts with database
- Generates SQL in migrations folder
- Creates migration metadata
**Output:**
```
src/db/migrations/
├── 0000_initial.sql
├── 0001_add_phone_number.sql
└── meta/
├── _journal.json
└── 0001_snapshot.json
```
### 3. Review Migration
**Always review** generated SQL before applying:
```sql
-- 0001_add_phone_number.sql
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);
```
### 4. Apply Migration
Execute migration against database:
```bash
npm run drizzle-kit migrate
```
**Or with explicit env loading:**
```bash
export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \
npm run drizzle-kit migrate
```
## Environment Loading Deep-Dive
### Why Environment Loading Matters
**Problem:** drizzle-kit runs as separate process, may not inherit env vars.
**Symptom:**
```
Error: url is undefined in dbCredentials
```
### Solution 1: Config File Loading (Recommended)
**drizzle.config.ts:**
```typescript
import { defineConfig } from 'drizzle-kit';
import { config } from 'dotenv';
config({ path: '.env.local' });
export default defineConfig({
schema: './src/db/schema.ts',
out: './src/db/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
```
**Key:** `config({ path: '.env.local' })` loads before exporting config.
### Solution 2: Shell Export
**Bash/Zsh:**
```bash
export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \
npm run drizzle-kit migrate
```
**Fish:**
```fish
set -x DATABASE_URL (grep DATABASE_URL .env.local | cut -d '=' -f2)
npm run drizzle-kit migrate
```
**PowerShell:**
```powershell
$env:DATABASE_URL = (Select-String -Path .env.local -Pattern "DATABASE_URL").Line.Split("=")[1]
npm run drizzle-kit migrate
```
### Solution 3: NPM Scripts
**package.json:**
```json
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "dotenv -e .env.local -- drizzle-kit migrate",
"db:push": "dotenv -e .env.local -- drizzle-kit push"
}
}
```
**Install dotenv-cli:**
```bash
npm add -D dotenv-cli
```
### Solution 4: Programmatic Migration
**scripts/migrate.ts:**
```typescript
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import { migrate } from 'drizzle-orm/neon-http/migrator';
import { config } from 'dotenv';
config({ path: '.env.local' });
const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql);
await migrate(db, { migrationsFolder: './src/db/migrations' });
console.log('Migrations complete');
```
**Run:**
```bash
tsx scripts/migrate.ts
```
## Migration Patterns
### Initial Setup
**First migration creates all tables:**
```sql
-- 0000_initial.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX posts_user_id_idx ON posts(user_id);
```
### Adding Columns
**Schema:**
```typescript
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull(),
phoneNumber: varchar('phone_number', { length: 20 }), // NEW
});
```
**Generated:**
```sql
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);
```
### Dropping Columns
**Schema:**
```typescript
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull(),
// removed: phoneNumber
});
```
**Generated:**
```sql
ALTER TABLE users DROP COLUMN phone_number;
```
**Warning:** Data loss! Back up first.
### Renaming Columns
**Problem:** Drizzle sees rename as drop + add (data loss).
**Schema:**
```typescript
export const users = pgTable('users', {
id: serial('id').primaryKey(),
fullName: varchar('full_name', { length: 255 }), // was 'name'
});
```
**Generated (WRONG):**
```sql
ALTER TABLE users DROP COLUMN name;
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
```
**Solution:** Manually edit migration:
```sql
-- Change to:
ALTER TABLE users RENAME COLUMN name TO full_name;
```
### Changing Column Types
**Schema:**
```typescript
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
views: bigint('views', { mode: 'number' }), // was integer
});
```
**Generated:**
```sql
ALTER TABLE posts ALTER COLUMN views TYPE BIGINT;
```
**Caution:** May require data migration if types incompatible.
### Adding Indexes
**Schema:**
```typescript
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
}, (table) => ({
titleIdx: index('posts_title_idx').on(table.title), // NEW
}));
```
**Generated:**
```sql
CREATE INDEX posts_title_idx ON posts(title);
```
### Adding Foreign Keys
**Schema:**
```typescript
export const comments = pgTable('comments', {
id: serial('id').primaryKey(),
postId: serial('post_id')
.notNull()
.references(() => posts.id), // NEW
content: text('content').notNull(),
});
```
**Generated:**
```sql
ALTER TABLE comments
ADD CONSTRAINT comments_post_id_fkey
FOREIGN KEY (post_id) REFERENCES posts(id);
```
### Adding Constraints
**Unique:**
```typescript
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
});
```
**Generated:**
```sql
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
```
**Check:**
```typescript
export const products = pgTable('products', {
id: serial('id').primaryKey(),
price: integer('price').notNull(),
}, (table) => ({
priceCheck: check('price_check', 'price >= 0'),
}));
```
**Generated:**
```sql
ALTER TABLE products ADD CONSTRAINT price_check CHECK (price >= 0);
```
## Advanced Patterns
### Data Migrations
**Scenario:** Add column with computed value from existing data.
**Step 1:** Generate migration:
```bash
npm run drizzle-kit generate
```
**Step 2:** Edit migration to add data transformation:
```sql
-- Add column
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
-- Populate with data
UPDATE users SET full_name = first_name || ' ' || last_name;
-- Make not null after population
ALTER TABLE users ALTER COLUMN full_name SET NOT NULL;
```
### Conditional Migrations
**Add IF NOT EXISTS for idempotency:**
```sql
ALTER TABLE users
ADD COLUMN IF NOT EXISTS phone_number VARCHAR(20);
CREATE INDEX IF NOT EXISTS posts_title_idx ON posts(title);
```
**Useful for:**
- Re-running migrations
- Partial deployments
- Development environments
### Multi-Step Migrations
**Scenario:** Rename with zero downtime.
**Migration 1 (Deploy this first):**
```sql
-- Add new column
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
-- Copy data
UPDATE users SET full_name = name;
```
**Application update:** Write to both `name` and `full_name`.
**Migration 2 (Deploy after apps updated):**
```sql
-- Make new column not null
ALTER TABLE users ALTER COLUMN full_name SET NOT NULL;
-- Drop old column
ALTER TABLE users DROP COLUMN name;
```
### Rollback Strategies
**Option 1: Down migrations (manual)**
Create reverse migration:
```sql
-- up.sql
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);
-- down.sql (create manually)
ALTER TABLE users DROP COLUMN phone_number;
```
**Option 2: Snapshot and restore**
**Before migration:**
```bash
pg_dump $DATABASE_URL > backup.sql
```
**If problems:**
```bash
psql $DATABASE_URL < backup.sql
```
**Option 3: Drizzle push (dev only)**
Reset to schema state:
```bash
npm run drizzle-kit push --force
```
**Warning:** Data loss in dev!
## Migration in CI/CD
### GitHub Actions Example
```yaml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Run migrations
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npm run db:migrate
- name: Deploy application
run: npm run deploy
```
### Vercel Example
**vercel.json:**
```json
{
"buildCommand": "npm run build && npm run db:migrate",
"env": {
"DATABASE_URL": "@database_url"
}
}
```
**package.json:**
```json
{
"scripts": {
"build": "next build",
"db:migrate": "drizzle-kit migrate"
}
}
```
### Safety Checks
**Pre-migration script:**
```typescript
// scripts/pre-migrate.ts
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql);
async function preMigrationChecks() {
try {
await sql`SELECT 1`;
console.log('✅ Database connection successful');
const tables = await sql`
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
`;
console.log(`✅ Found ${tables.length} tables`);
return true;
} catch (err) {
console.error('❌ Pre-migration check failed:', err);
process.exit(1);
}
}
preMigrationChecks();
```
## Common Migration Errors
### Error: "migration already applied"
**Cause:** Journal shows migration as applied.
**Solution:**
```bash
# Check journal
cat src/db/migrations/meta/_journal.json
# Remove entry if needed (dev only!)
# Or regenerate migrations
rm -rf src/db/migrations/*
npm run drizzle-kit generate
```
### Error: "column already exists"
**Cause:** Schema out of sync with database.
**Solutions:**
**Option 1:** Edit migration to use IF NOT EXISTS:
```sql
ALTER TABLE users
ADD COLUMN IF NOT EXISTS phone_number VARCHAR(20);
```
**Option 2:** Reset migrations (dev only):
```bash
npm run drizzle-kit drop # Drops all tables!
npm run drizzle-kit migrate
```
### Error: "violates foreign key constraint"
**Cause:** Trying to drop table referenced by foreign keys.
**Solution:** Drop in reverse dependency order:
```sql
DROP TABLE comments; -- First (depends on posts)
DROP TABLE posts; -- Then (depends on users)
DROP TABLE users; -- Finally
```
Or use CASCADE (data loss!):
```sql
DROP TABLE users CASCADE;
```
### Error: "cannot drop column"
**Cause:** Column referenced by views, functions, or constraints.
**Solution:**
```sql
-- Find dependencies
SELECT * FROM information_schema.view_column_usage
WHERE column_name = 'your_column';
-- Drop views first
DROP VIEW view_name;
-- Then drop column
ALTER TABLE users DROP COLUMN your_column;
```
## Best Practices
### 1. Always Review Generated SQL
Don't blindly apply migrations:
```bash
# Generate
npm run drizzle-kit generate
# Review
cat src/db/migrations/0001_*.sql
# Apply only after review
npm run drizzle-kit migrate
```
### 2. Test Migrations in Development
**Before production:**
```bash
# On dev database
export DATABASE_URL=$DEV_DATABASE_URL
npm run db:migrate
# Test application
npm run test
# Only then deploy to production
```
### 3. Back Up Before Major Migrations
```bash
pg_dump $DATABASE_URL > backup_$(date +%Y%m%d).sql
```
### 4. Use Transactions (when possible)
Wrap multiple operations:
```sql
BEGIN;
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);
UPDATE users SET phone_number = '000-000-0000' WHERE phone_number IS NULL;
ALTER TABLE users ALTER COLUMN phone_number SET NOT NULL;
COMMIT;
```
### 5. Document Breaking Changes
Add comments in migration files:
```sql
-- Breaking change: Removing deprecated 'username' column
-- Applications must use 'email' instead
-- Migration date: 2024-01-15
ALTER TABLE users DROP COLUMN username;
```
### 6. Keep Migrations Small
One logical change per migration:
- ✅ Good: "Add phone number column"
- ❌ Bad: "Add phone number, refactor users table, update indexes"
## Related Resources
- `guides/troubleshooting.md` - Migration error solutions
- `guides/schema-only.md` - Schema change patterns
- `references/adapters.md` - Connection configuration
- Scripts: `scripts/run-migration.ts`

View File

@@ -0,0 +1,761 @@
# Query Patterns Reference Guide
Complete reference for querying with Drizzle ORM.
## Table of Contents
- [Basic CRUD Operations](#basic-crud-operations)
- [Advanced Filtering](#advanced-filtering)
- [Joins and Relations](#joins-and-relations)
- [Aggregations](#aggregations)
- [Subqueries](#subqueries)
- [Transactions](#transactions)
- [Batch Operations](#batch-operations)
- [Raw SQL](#raw-sql)
- [Performance Optimization](#performance-optimization)
- [Type Safety](#type-safety)
- [Common Patterns](#common-patterns)
- [Related Resources](#related-resources)
---
## Basic CRUD Operations
### Create (Insert)
**Single record:**
```typescript
import { db } from './db';
import { users } from './db/schema';
const newUser = await db.insert(users)
.values({
email: 'user@example.com',
name: 'John Doe',
})
.returning();
console.log(newUser[0]); // { id: 1, email: '...', name: '...' }
```
**Multiple records:**
```typescript
const newUsers = await db.insert(users)
.values([
{ email: 'user1@example.com', name: 'User 1' },
{ email: 'user2@example.com', name: 'User 2' },
{ email: 'user3@example.com', name: 'User 3' },
])
.returning();
```
**With onConflictDoNothing:**
```typescript
await db.insert(users)
.values({ email: 'user@example.com', name: 'John' })
.onConflictDoNothing();
```
**With onConflictDoUpdate (upsert):**
```typescript
await db.insert(users)
.values({ email: 'user@example.com', name: 'John' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'John Updated' },
});
```
### Read (Select)
**All records:**
```typescript
const allUsers = await db.select().from(users);
```
**Specific columns:**
```typescript
const userEmails = await db.select({
id: users.id,
email: users.email,
}).from(users);
```
**With WHERE clause:**
```typescript
import { eq, gt, lt, like, and, or } from 'drizzle-orm';
const user = await db.select()
.from(users)
.where(eq(users.email, 'user@example.com'));
const activeUsers = await db.select()
.from(users)
.where(eq(users.isActive, true));
```
**Multiple conditions:**
```typescript
const filteredUsers = await db.select()
.from(users)
.where(
and(
eq(users.isActive, true),
gt(users.createdAt, new Date('2024-01-01'))
)
);
```
**With LIMIT and OFFSET:**
```typescript
const paginatedUsers = await db.select()
.from(users)
.limit(10)
.offset(20); // Page 3
```
**With ORDER BY:**
```typescript
const sortedUsers = await db.select()
.from(users)
.orderBy(users.createdAt); // ASC by default
import { desc } from 'drizzle-orm';
const recentUsers = await db.select()
.from(users)
.orderBy(desc(users.createdAt));
```
### Update
**Single record:**
```typescript
await db.update(users)
.set({ name: 'Jane Doe' })
.where(eq(users.id, 1));
```
**Multiple records:**
```typescript
await db.update(users)
.set({ isActive: false })
.where(eq(users.deletedAt, null));
```
**With returning:**
```typescript
const updated = await db.update(users)
.set({ name: 'Updated Name' })
.where(eq(users.id, 1))
.returning();
```
**Partial updates:**
```typescript
const updates: Partial<typeof users.$inferSelect> = {
name: 'New Name',
};
await db.update(users)
.set(updates)
.where(eq(users.id, 1));
```
### Delete
**Single record:**
```typescript
await db.delete(users)
.where(eq(users.id, 1));
```
**Multiple records:**
```typescript
await db.delete(users)
.where(eq(users.isActive, false));
```
**With returning:**
```typescript
const deleted = await db.delete(users)
.where(eq(users.id, 1))
.returning();
```
**Soft delete (recommended):**
```typescript
await db.update(users)
.set({ deletedAt: new Date() })
.where(eq(users.id, 1));
```
## Advanced Filtering
### Comparison Operators
```typescript
import { eq, ne, gt, gte, lt, lte } from 'drizzle-orm';
const adults = await db.select()
.from(users)
.where(gte(users.age, 18));
const recentPosts = await db.select()
.from(posts)
.where(gt(posts.createdAt, new Date('2024-01-01')));
const excludeAdmin = await db.select()
.from(users)
.where(ne(users.role, 'admin'));
```
### Pattern Matching
```typescript
import { like, ilike } from 'drizzle-orm';
const gmailUsers = await db.select()
.from(users)
.where(like(users.email, '%@gmail.com'));
const searchByName = await db.select()
.from(users)
.where(ilike(users.name, '%john%')); // Case-insensitive
```
### NULL Checks
```typescript
import { isNull, isNotNull } from 'drizzle-orm';
const usersWithPhone = await db.select()
.from(users)
.where(isNotNull(users.phoneNumber));
const unverifiedUsers = await db.select()
.from(users)
.where(isNull(users.emailVerifiedAt));
```
### IN Operator
```typescript
import { inArray } from 'drizzle-orm';
const specificUsers = await db.select()
.from(users)
.where(inArray(users.id, [1, 2, 3, 4, 5]));
```
### BETWEEN
```typescript
import { between } from 'drizzle-orm';
const postsThisMonth = await db.select()
.from(posts)
.where(
between(
posts.createdAt,
new Date('2024-01-01'),
new Date('2024-01-31')
)
);
```
### Complex Conditions
```typescript
import { and, or, not } from 'drizzle-orm';
const complexQuery = await db.select()
.from(users)
.where(
or(
and(
eq(users.isActive, true),
gte(users.age, 18)
),
eq(users.role, 'admin')
)
);
```
## Joins and Relations
### Manual Joins
**Inner join:**
```typescript
const postsWithAuthors = await db.select({
postId: posts.id,
postTitle: posts.title,
authorName: users.name,
authorEmail: users.email,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id));
```
**Left join:**
```typescript
const allPostsWithOptionalAuthors = await db.select()
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id));
```
### Relational Queries (Recommended)
**Define relations first:**
```typescript
import { relations } from 'drizzle-orm';
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
```
**Query with relations:**
```typescript
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
});
console.log(usersWithPosts[0].posts); // Array of posts
```
**Nested relations:**
```typescript
const postsWithAuthorsAndComments = await db.query.posts.findMany({
with: {
author: true,
comments: {
with: {
author: true,
},
},
},
});
```
**Filtered relations:**
```typescript
const usersWithRecentPosts = await db.query.users.findMany({
with: {
posts: {
where: gt(posts.createdAt, new Date('2024-01-01')),
orderBy: desc(posts.createdAt),
limit: 5,
},
},
});
```
**Partial selection:**
```typescript
const usersWithPostTitles = await db.query.users.findMany({
columns: {
id: true,
name: true,
},
with: {
posts: {
columns: {
id: true,
title: true,
},
},
},
});
```
## Aggregations
### Count
```typescript
import { count } from 'drizzle-orm';
const userCount = await db.select({
count: count(),
}).from(users);
console.log(userCount[0].count); // Total users
```
**Count with grouping:**
```typescript
const postsByAuthor = await db.select({
authorId: posts.authorId,
postCount: count(),
})
.from(posts)
.groupBy(posts.authorId);
```
### Sum, Avg, Min, Max
```typescript
import { sum, avg, min, max } from 'drizzle-orm';
const stats = await db.select({
totalViews: sum(posts.views),
avgViews: avg(posts.views),
minViews: min(posts.views),
maxViews: max(posts.views),
}).from(posts);
```
### Having
```typescript
const activeAuthors = await db.select({
authorId: posts.authorId,
postCount: count(),
})
.from(posts)
.groupBy(posts.authorId)
.having(gt(count(), 5)); // Authors with > 5 posts
```
## Subqueries
### In WHERE clause
```typescript
const activeUserIds = db.select({ id: users.id })
.from(users)
.where(eq(users.isActive, true));
const postsFromActiveUsers = await db.select()
.from(posts)
.where(inArray(posts.authorId, activeUserIds));
```
### As derived table
```typescript
const recentPosts = db.select()
.from(posts)
.where(gt(posts.createdAt, new Date('2024-01-01')))
.as('recentPosts');
const authorsOfRecentPosts = await db.select()
.from(users)
.innerJoin(recentPosts, eq(users.id, recentPosts.authorId));
```
## Transactions
**Only available with WebSocket adapter!**
```typescript
await db.transaction(async (tx) => {
const user = await tx.insert(users)
.values({ email: 'user@example.com', name: 'John' })
.returning();
await tx.insert(posts)
.values({
authorId: user[0].id,
title: 'First post',
content: 'Hello world',
});
});
```
**With error handling:**
```typescript
try {
await db.transaction(async (tx) => {
await tx.insert(users).values({ email: 'user@example.com' });
await tx.insert(posts).values({ title: 'Post' });
throw new Error('Rollback!'); // Transaction rolls back
});
} catch (err) {
console.error('Transaction failed:', err);
}
```
**Nested transactions:**
```typescript
await db.transaction(async (tx) => {
await tx.insert(users).values({ email: 'user1@example.com' });
await tx.transaction(async (tx2) => {
await tx2.insert(posts).values({ title: 'Post 1' });
});
});
```
## Batch Operations
**HTTP adapter alternative to transactions:**
```typescript
await db.batch([
db.insert(users).values({ email: 'user1@example.com' }),
db.insert(users).values({ email: 'user2@example.com' }),
db.insert(posts).values({ title: 'Post 1' }),
]);
```
**Note:** Not atomic! Use transactions if you need rollback capability.
## Raw SQL
### Execute raw query
```typescript
import { sql } from 'drizzle-orm';
const result = await db.execute(sql`
SELECT * FROM users
WHERE email LIKE ${'%@gmail.com'}
`);
```
### SQL in WHERE clause
```typescript
const users = await db.select()
.from(users)
.where(sql`${users.email} LIKE '%@gmail.com'`);
```
### SQL expressions
```typescript
const posts = await db.select({
id: posts.id,
title: posts.title,
excerpt: sql<string>`LEFT(${posts.content}, 100)`,
}).from(posts);
```
### Custom functions
```typescript
const searchResults = await db.select()
.from(posts)
.where(
sql`to_tsvector('english', ${posts.content}) @@ to_tsquery('english', ${'search query'})`
);
```
## Performance Optimization
### Select only needed columns
**Bad:**
```typescript
const users = await db.select().from(users); // All columns
```
**Good:**
```typescript
const users = await db.select({
id: users.id,
email: users.email,
}).from(users);
```
### Use indexes
**Ensure indexed columns in WHERE:**
```typescript
// Assuming index on users.email
const user = await db.select()
.from(users)
.where(eq(users.email, 'user@example.com')); // Fast
```
### Avoid N+1 queries
**Bad:**
```typescript
const posts = await db.select().from(posts);
for (const post of posts) {
const author = await db.select()
.from(users)
.where(eq(users.id, post.authorId)); // N queries!
}
```
**Good:**
```typescript
const posts = await db.query.posts.findMany({
with: {
author: true, // Single query with join
},
});
```
### Use pagination
```typescript
async function getPaginatedUsers(page: number, pageSize: number = 10) {
return db.select()
.from(users)
.limit(pageSize)
.offset((page - 1) * pageSize);
}
```
### Batch inserts
**Bad:**
```typescript
for (const user of users) {
await db.insert(users).values(user); // N queries
}
```
**Good:**
```typescript
await db.insert(users).values(users); // Single query
```
## Type Safety
### Infer types from schema
```typescript
type User = typeof users.$inferSelect;
type NewUser = typeof users.$inferInsert;
const user: User = {
id: 1,
email: 'user@example.com',
name: 'John',
createdAt: new Date(),
};
const newUser: NewUser = {
email: 'user@example.com',
name: 'John',
};
```
### Type-safe WHERE conditions
```typescript
function getUsersByStatus(status: User['status']) {
return db.select()
.from(users)
.where(eq(users.status, status));
}
```
### Type-safe updates
```typescript
function updateUser(id: number, data: Partial<NewUser>) {
return db.update(users)
.set(data)
.where(eq(users.id, id))
.returning();
}
```
## Common Patterns
### Soft deletes
**Schema:**
```typescript
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
deletedAt: timestamp('deleted_at'),
});
```
**Queries:**
```typescript
const activePosts = await db.select()
.from(posts)
.where(isNull(posts.deletedAt));
const deletedPosts = await db.select()
.from(posts)
.where(isNotNull(posts.deletedAt));
```
### Timestamps
**Auto-update:**
```typescript
async function updatePost(id: number, data: Partial<NewPost>) {
return db.update(posts)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(posts.id, id))
.returning();
}
```
### Search
**Simple search:**
```typescript
const searchUsers = await db.select()
.from(users)
.where(
or(
ilike(users.name, `%${query}%`),
ilike(users.email, `%${query}%`)
)
);
```
**Full-text search:**
```typescript
const searchPosts = await db.select()
.from(posts)
.where(
sql`to_tsvector('english', ${posts.title} || ' ' || ${posts.content}) @@ plainto_tsquery('english', ${query})`
);
```
### Unique constraints
**Handle duplicates:**
```typescript
try {
await db.insert(users).values({ email: 'user@example.com' });
} catch (err) {
if (err.code === '23505') { // Unique violation
console.error('Email already exists');
}
}
```
**Or use upsert:**
```typescript
await db.insert(users)
.values({ email: 'user@example.com', name: 'John' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'John Updated' },
});
```
## Related Resources
- `guides/schema-only.md` - Schema design patterns
- `references/adapters.md` - Transaction availability by adapter
- `guides/troubleshooting.md` - Query error solutions
- `templates/schema-example.ts` - Complete schema with relations