Initial commit
This commit is contained in:
478
skills/neon-drizzle/references/adapters.md
Normal file
478
skills/neon-drizzle/references/adapters.md
Normal 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
|
||||
652
skills/neon-drizzle/references/migrations.md
Normal file
652
skills/neon-drizzle/references/migrations.md
Normal 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`
|
||||
761
skills/neon-drizzle/references/query-patterns.md
Normal file
761
skills/neon-drizzle/references/query-patterns.md
Normal 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
|
||||
Reference in New Issue
Block a user