Initial commit
This commit is contained in:
79
skills/neon-drizzle/SKILL.md
Normal file
79
skills/neon-drizzle/SKILL.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: neon-drizzle
|
||||
description: Creates a fully functional Drizzle ORM setup with a provisioned Neon database. Installs dependencies, provisions database credentials, configures connections, generates schemas, and runs migrations. Results in working code that can immediately connect to and query the database. Use when creating new projects with Drizzle, adding ORM to existing applications, or modifying database schemas.
|
||||
allowed-tools: ["bash", "write", "read_file"]
|
||||
---
|
||||
|
||||
# Neon Drizzle Integration
|
||||
|
||||
Comprehensive Drizzle ORM setup for Neon databases with guided workflows.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Setting up Drizzle in a new project (Next.js, Vite, Express, etc.)
|
||||
- Integrating Drizzle into an existing application
|
||||
- Creating or modifying database schemas
|
||||
- Troubleshooting migration issues
|
||||
|
||||
## Code Generation Rules
|
||||
|
||||
When generating TypeScript/JavaScript code:
|
||||
- BEFORE generating import statements, check tsconfig.json for path aliases (compilerOptions.paths)
|
||||
- If path aliases exist (e.g., "@/*": ["./src/*"]), use them (e.g., import { x } from '@/lib/utils')
|
||||
- If NO path aliases exist or unsure, ALWAYS use relative imports (e.g., import { x } from '../../../lib/utils')
|
||||
- Verify imports match the project's configuration
|
||||
- Default to relative imports - they always work regardless of configuration
|
||||
|
||||
## Available Guides
|
||||
|
||||
Each guide is a complete, self-contained walkthrough with numbered phases:
|
||||
|
||||
- **`guides/new-project.md`** - Full setup from scratch (see: Table of Contents)
|
||||
- **`guides/existing-project.md`** - Add Drizzle to running apps (see: Table of Contents)
|
||||
- **`guides/schema-only.md`** - Schema creation and modification (see: Table of Contents)
|
||||
- **`guides/troubleshooting.md`** - Debug common issues (organized by error type)
|
||||
|
||||
I'll automatically detect your context (package manager, framework, deployment target) and select the appropriate guide based on your request.
|
||||
|
||||
## Quick Examples
|
||||
|
||||
Tell me what you're building - I'll handle the rest:
|
||||
|
||||
- "Setup Drizzle for my Next.js blog on Vercel" → Auto-detects Vercel + Next.js → HTTP adapter
|
||||
- "Add Drizzle to my Express API" → Auto-detects Node.js server → WebSocket adapter
|
||||
- "Create a users table with auth fields" → Loads schema guide → Generates schema
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
For deeper technical details (loaded on-demand):
|
||||
|
||||
- `references/adapters.md` - HTTP vs WebSocket decision guide
|
||||
- `references/migrations.md` - Migration patterns and troubleshooting
|
||||
- `references/query-patterns.md` - Example queries and best practices
|
||||
|
||||
## Templates & Scripts
|
||||
|
||||
- `templates/schema-example.ts` - Multi-table schema with relations
|
||||
- `templates/drizzle-config.ts` - Configuration examples
|
||||
- `scripts/generate-schema.ts` - Automated migration generation
|
||||
- `scripts/run-migration.ts` - Programmatic migration runner
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **neon-serverless** - Connection setup without ORM
|
||||
- **neon-toolkit** - Ephemeral databases for testing
|
||||
- **add-neon-docs** - Add Neon best practices to your project (run after setup)
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
I will:
|
||||
1. Detect your project context automatically
|
||||
2. Select and load the appropriate guide
|
||||
3. Follow the guide's phases sequentially
|
||||
4. Track progress using the guide's workflow checklist
|
||||
5. Load reference files only when needed
|
||||
6. Offer to add Neon best practices to your project docs
|
||||
|
||||
Ready to get started? Just describe what you're building!
|
||||
398
skills/neon-drizzle/guides/existing-project.md
Normal file
398
skills/neon-drizzle/guides/existing-project.md
Normal file
@@ -0,0 +1,398 @@
|
||||
# Existing Project Integration
|
||||
|
||||
> **Complete Walkthrough**: This is a self-contained, step-by-step guide with its own numbered phases (Phase 1-8).
|
||||
> Follow each phase in order to safely add Drizzle to your existing application.
|
||||
|
||||
Guide for adding Drizzle ORM to an existing application with Neon.
|
||||
|
||||
### Important:
|
||||
- Remember to run the neon-plugin:add-neon-docs skill with the parameter SKILL_NAME="neon-drizzle" after completing the guide.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Workflow Checklist](#workflow-checklist)
|
||||
- [Phase 1: Pre-Integration Check](#phase-1-pre-integration-check)
|
||||
- [Phase 2: Incremental Installation](#phase-2-incremental-installation)
|
||||
- [Phase 3: Configuration](#phase-3-configuration)
|
||||
- [Phase 4: Schema Strategy](#phase-4-schema-strategy)
|
||||
- [Phase 5: Migration Handling](#phase-5-migration-handling)
|
||||
- [Phase 6: Coexistence Patterns](#phase-6-coexistence-patterns)
|
||||
- [Phase 7: Verification](#phase-7-verification)
|
||||
- [Phase 8: Add Best Practices References](#phase-8-add-best-practices-references)
|
||||
|
||||
---
|
||||
|
||||
## Workflow Checklist
|
||||
|
||||
When following this guide, I will track these high-level tasks:
|
||||
|
||||
- [ ] Pre-integration check (detect existing ORMs, database schema, environment)
|
||||
- [ ] Install Drizzle dependencies without disrupting existing setup
|
||||
- [ ] Create isolated Drizzle configuration (separate from existing code)
|
||||
- [ ] Choose and implement schema strategy (new tables vs mirroring existing)
|
||||
- [ ] Handle migrations safely based on schema strategy
|
||||
- [ ] Set up coexistence patterns and gradual migration approach
|
||||
- [ ] Verify Drizzle integration without breaking existing functionality
|
||||
- [ ] Add Neon Drizzle best practices to project docs
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Pre-Integration Check
|
||||
|
||||
Before adding Drizzle, check for conflicts:
|
||||
|
||||
### 1.1. Check for Other ORMs
|
||||
|
||||
```bash
|
||||
grep -E '"(prisma|typeorm|sequelize|mongoose)"' package.json
|
||||
```
|
||||
|
||||
**If found:**
|
||||
- Consider migration strategy (coexistence vs replacement)
|
||||
- Document which tables use which ORM
|
||||
- Plan gradual migration if needed
|
||||
|
||||
### 1.2. Check Database Schema
|
||||
|
||||
Connect to your database and verify existing tables:
|
||||
```bash
|
||||
psql $DATABASE_URL -c "\dt"
|
||||
```
|
||||
|
||||
**Important:** Note existing tables - Drizzle should not conflict with them.
|
||||
|
||||
### 1.3. Check Environment Setup
|
||||
|
||||
```bash
|
||||
ls .env .env.local .env.production
|
||||
grep DATABASE_URL .env*
|
||||
```
|
||||
|
||||
**If DATABASE_URL exists:**
|
||||
- Verify connection string format is compatible with Neon (`postgresql://...`)
|
||||
- If it's a different database provider, you'll need to migrate or provision a Neon database
|
||||
|
||||
**If DATABASE_URL does NOT exist:**
|
||||
Follow the database provisioning steps from `guides/new-project.md` Phase 3.1:
|
||||
1. List the projects using the neon MCP Server to check existing projects
|
||||
2. Create a new project using the neon MCP Server if needed
|
||||
3. Get the connection string using the neon MCP Server
|
||||
4. Write to appropriate environment file (.env.local for Next.js, .env for others)
|
||||
5. Add environment file to .gitignore
|
||||
|
||||
## Phase 2: Incremental Installation
|
||||
|
||||
Add Drizzle without disrupting existing setup:
|
||||
|
||||
### 2.1. Install Dependencies
|
||||
|
||||
**For Vercel/Edge:**
|
||||
```bash
|
||||
[package-manager] add drizzle-orm @neondatabase/serverless
|
||||
[package-manager] add -D drizzle-kit dotenv
|
||||
```
|
||||
|
||||
**For Node.js:**
|
||||
```bash
|
||||
[package-manager] add drizzle-orm @neondatabase/serverless ws
|
||||
[package-manager] add -D drizzle-kit dotenv @types/ws
|
||||
```
|
||||
|
||||
### 2.2. Create Isolated Drizzle Directory
|
||||
|
||||
Keep Drizzle separate from existing code:
|
||||
```bash
|
||||
mkdir -p src/drizzle
|
||||
```
|
||||
|
||||
Structure:
|
||||
```
|
||||
src/drizzle/
|
||||
├── index.ts # Connection
|
||||
├── schema.ts # New schemas only
|
||||
└── migrations/ # Drizzle migrations
|
||||
```
|
||||
|
||||
## Phase 3: Configuration
|
||||
|
||||
### 3.1. Create Drizzle Config
|
||||
|
||||
Create `drizzle.config.ts` with explicit environment loading:
|
||||
|
||||
**CRITICAL:** The `config({ path: '...' })` must match your environment file name.
|
||||
|
||||
**For Next.js (using .env.local):**
|
||||
```typescript
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
// Load .env.local explicitly
|
||||
config({ path: '.env.local' });
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/drizzle/schema.ts',
|
||||
out: './src/drizzle/migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**For other projects (using .env):**
|
||||
```typescript
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
// Load .env explicitly
|
||||
config({ path: '.env' });
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/drizzle/schema.ts',
|
||||
out: './src/drizzle/migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Point schema and migrations to `src/drizzle/` to avoid conflicts with existing code
|
||||
- Explicit dotenv path prevents "url: undefined" errors during migrations
|
||||
|
||||
### 3.2. Create Connection
|
||||
|
||||
`src/drizzle/index.ts` - Choose adapter based on environment (see `references/adapters.md`):
|
||||
|
||||
**HTTP (Vercel/Edge):**
|
||||
```typescript
|
||||
import { drizzle } from 'drizzle-orm/neon-http';
|
||||
import { neon } from '@neondatabase/serverless';
|
||||
|
||||
const sql = neon(process.env.DATABASE_URL!);
|
||||
export const drizzleDb = drizzle(sql);
|
||||
```
|
||||
|
||||
**WebSocket (Node.js):**
|
||||
```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! });
|
||||
export const drizzleDb = drizzle(pool);
|
||||
```
|
||||
|
||||
**Important:** Name export as `drizzleDb` to avoid conflicts with existing `db` exports.
|
||||
|
||||
## Phase 4: Schema Strategy
|
||||
|
||||
Choose integration approach:
|
||||
|
||||
### 4.1. Option A: New Tables Only
|
||||
|
||||
Create schemas for new features only, leave existing tables alone:
|
||||
|
||||
`src/drizzle/schema.ts`:
|
||||
```typescript
|
||||
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const newFeatureTable = pgTable('new_feature', {
|
||||
id: serial('id').primaryKey(),
|
||||
data: text('data').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
});
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- No migration of existing data
|
||||
- Zero risk to current functionality
|
||||
- Gradual adoption
|
||||
|
||||
**Cons:**
|
||||
- Mixed query patterns (Drizzle + existing ORM)
|
||||
- Two connection patterns in codebase
|
||||
|
||||
### 4.2. Option B: Mirror Existing Tables
|
||||
|
||||
Define schemas for existing tables to gradually migrate queries:
|
||||
|
||||
```typescript
|
||||
import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const existingUsers = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull(),
|
||||
name: varchar('name', { length: 255 }),
|
||||
createdAt: timestamp('created_at'),
|
||||
});
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Can query existing data with Drizzle
|
||||
- Gradually replace old ORM queries
|
||||
- Type-safe access to existing tables
|
||||
|
||||
**Cons:**
|
||||
- Must match existing schema exactly
|
||||
- Requires careful migration strategy
|
||||
|
||||
### 4.3. Recommended: Hybrid Approach
|
||||
|
||||
1. Start with Option A (new tables only)
|
||||
2. Once comfortable, add schemas for frequently-queried existing tables (Option B)
|
||||
3. Gradually migrate queries from old ORM to Drizzle
|
||||
4. Eventually remove old ORM
|
||||
|
||||
## Phase 5: Migration Handling
|
||||
|
||||
### 5.1. For New Tables
|
||||
|
||||
Generate and run migrations normally:
|
||||
```bash
|
||||
[package-manager] drizzle-kit generate
|
||||
export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \
|
||||
[package-manager] drizzle-kit migrate
|
||||
```
|
||||
|
||||
### 5.2. For Existing Tables
|
||||
|
||||
**Do NOT run migrations** - tables already exist!
|
||||
|
||||
Instead, use Drizzle schemas for querying only:
|
||||
```typescript
|
||||
import { drizzleDb } from './drizzle';
|
||||
import { existingUsers } from './drizzle/schema';
|
||||
|
||||
const users = await drizzleDb.select().from(existingUsers);
|
||||
```
|
||||
|
||||
### 5.3. Mixed Scenario
|
||||
|
||||
If you have both new and existing tables:
|
||||
1. Define all schemas in `schema.ts`
|
||||
2. Run `drizzle-kit generate`
|
||||
3. **Manually edit** generated migration to remove SQL for existing tables
|
||||
4. Apply migration
|
||||
|
||||
See `references/migrations.md` for advanced patterns.
|
||||
|
||||
### 5.4. Add Migration Scripts
|
||||
|
||||
Add these convenience scripts to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
npm run db:generate # Generate migrations from schema changes
|
||||
npm run db:migrate # Apply pending migrations
|
||||
npm run db:push # Push schema directly (dev only)
|
||||
npm run db:studio # Open Drizzle Studio
|
||||
```
|
||||
|
||||
**Note:** Replace `npm run` with your package manager's equivalent (`pnpm`, `yarn`, `bun`).
|
||||
|
||||
## Phase 6: Coexistence Patterns
|
||||
|
||||
### 6.1. Naming Conventions
|
||||
|
||||
Keep clear separation:
|
||||
```typescript
|
||||
import { db as prismaDb } from './lib/prisma';
|
||||
import { drizzleDb } from './drizzle';
|
||||
|
||||
const prismaUsers = await prismaDb.user.findMany();
|
||||
const drizzleFeatures = await drizzleDb.select().from(newFeatureTable);
|
||||
```
|
||||
|
||||
### 6.2. Gradual Migration
|
||||
|
||||
**Step 1:** New features use Drizzle
|
||||
```typescript
|
||||
async function createFeature(data: NewFeatureInput) {
|
||||
return drizzleDb.insert(newFeatureTable).values(data).returning();
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2:** Migrate read queries (safe, no data changes)
|
||||
```typescript
|
||||
async function getUsers() {
|
||||
return drizzleDb.select().from(existingUsers);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3:** Migrate write queries (after thorough testing)
|
||||
```typescript
|
||||
async function updateUser(id: number, data: UserUpdate) {
|
||||
return drizzleDb.update(existingUsers)
|
||||
.set(data)
|
||||
.where(eq(existingUsers.id, id));
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4:** Remove old ORM once all queries migrated
|
||||
|
||||
## Phase 7: Verification
|
||||
|
||||
Test integration without breaking existing functionality:
|
||||
|
||||
### 7.1. Test New Tables
|
||||
|
||||
```typescript
|
||||
import { drizzleDb } from './drizzle';
|
||||
import { newFeatureTable } from './drizzle/schema';
|
||||
|
||||
const result = await drizzleDb.insert(newFeatureTable)
|
||||
.values({ data: 'test' })
|
||||
.returning();
|
||||
|
||||
console.log('New table works:', result);
|
||||
```
|
||||
|
||||
### 7.2. Test Existing Tables (if mirrored)
|
||||
|
||||
```typescript
|
||||
import { drizzleDb } from './drizzle';
|
||||
import { existingUsers } from './drizzle/schema';
|
||||
|
||||
const users = await drizzleDb.select().from(existingUsers);
|
||||
console.log('Existing table accessible:', users);
|
||||
```
|
||||
|
||||
### 7.3. Verify Old ORM Still Works
|
||||
|
||||
```typescript
|
||||
import { db as oldDb } from './lib/your-orm';
|
||||
|
||||
const oldQuery = await oldDb.users.findMany();
|
||||
console.log('Old ORM still works:', oldQuery);
|
||||
```
|
||||
|
||||
## Phase 8: Add Best Practices References
|
||||
|
||||
Before executing the add-neon-docs skill, provide a summary of everything that has been done:
|
||||
|
||||
"✅ ... Drizzle integration is complete! Now adding documentation references..."
|
||||
|
||||
Then execute the neon-plugin:add-neon-docs skill with the parameter SKILL_NAME="neon-drizzle"
|
||||
|
||||
This will add reference links to Neon + Drizzle best practices documentation in your project's AI documentation file, helping AI assistants provide better guidance in future conversations.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Integration Complete!
|
||||
|
||||
Your Drizzle integration with the existing project is ready to use.
|
||||
|
||||
312
skills/neon-drizzle/guides/new-project.md
Normal file
312
skills/neon-drizzle/guides/new-project.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# New Project Setup
|
||||
|
||||
> **Complete Walkthrough**: This is a self-contained, step-by-step guide with its own numbered phases (Phase 1-6).
|
||||
> Follow each phase in order for a full Drizzle + Neon setup from scratch.
|
||||
|
||||
Complete guide for setting up Drizzle ORM with Neon from scratch.
|
||||
|
||||
### Important:
|
||||
- Remember to run the neon-plugin:add-neon-docs skill with the parameter SKILL_NAME="neon-drizzle" after completing the guide.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [New Project Setup](#new-project-setup)
|
||||
- [Important:](#important)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Workflow Checklist](#workflow-checklist)
|
||||
- [Phase 1: Context Detection](#phase-1-context-detection)
|
||||
- [Phase 2: Installation](#phase-2-installation)
|
||||
- [Phase 3: Configuration](#phase-3-configuration)
|
||||
- [3.1. Neon Database Provisioning \& Environment File](#31-neon-database-provisioning--environment-file)
|
||||
- [3.2. Drizzle Config](#32-drizzle-config)
|
||||
- [3.3. Database Connection](#33-database-connection)
|
||||
- [Phase 4: Schema Generation](#phase-4-schema-generation)
|
||||
- [4.1. Common Patterns](#41-common-patterns)
|
||||
- [Phase 5: Migrations](#phase-5-migrations)
|
||||
- [5.1. Generate Migration](#51-generate-migration)
|
||||
- [5.2. Apply Migration](#52-apply-migration)
|
||||
- [5.3. Add Migration Scripts](#53-add-migration-scripts)
|
||||
- [5.4. If Migration Fails](#54-if-migration-fails)
|
||||
- [Phase 6: Add Best Practices References](#phase-6-add-best-practices-references)
|
||||
- [✅ Setup Complete!](#-setup-complete)
|
||||
|
||||
---
|
||||
|
||||
## Workflow Checklist
|
||||
|
||||
When following this guide, I will track these high-level tasks:
|
||||
|
||||
- [ ] Detect project context (package manager, framework, existing setup)
|
||||
- [ ] Install Drizzle dependencies based on deployment target
|
||||
- [ ] Provision Neon database (list projects, create if needed, get connection string)
|
||||
- [ ] Write connection string to environment file and verify
|
||||
- [ ] Create Drizzle configuration files (drizzle.config.ts, db connection)
|
||||
- [ ] Generate schema based on app type
|
||||
- [ ] Run and verify migrations
|
||||
- [ ] Add Neon Drizzle best practices to project docs
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Context Detection
|
||||
|
||||
Auto-detect project context:
|
||||
|
||||
**Check Package Manager:**
|
||||
```bash
|
||||
ls package-lock.json # → npm
|
||||
ls bun.lockb # → bun
|
||||
ls pnpm-lock.yaml # → pnpm
|
||||
ls yarn.lock # → yarn
|
||||
```
|
||||
|
||||
**Check Framework:**
|
||||
```bash
|
||||
grep '"next"' package.json # → Next.js
|
||||
grep '"express"' package.json # → Express
|
||||
grep '"vite"' package.json # → Vite
|
||||
```
|
||||
|
||||
**Check Existing Setup:**
|
||||
```bash
|
||||
ls drizzle.config.ts # Already configured?
|
||||
ls src/db/schema.ts # Schema exists?
|
||||
```
|
||||
|
||||
**Check Environment Files:**
|
||||
```bash
|
||||
ls .env .env.local .env.production
|
||||
```
|
||||
|
||||
## Phase 2: Installation
|
||||
|
||||
Based on detection, install dependencies:
|
||||
|
||||
**For Vercel/Edge Environments (Next.js, Vite on Vercel):**
|
||||
```bash
|
||||
[package-manager] add drizzle-orm @neondatabase/serverless
|
||||
[package-manager] add -D drizzle-kit dotenv @vercel/node
|
||||
```
|
||||
|
||||
**For Node.js Servers (Express, Fastify, standard Node):**
|
||||
```bash
|
||||
[package-manager] add drizzle-orm @neondatabase/serverless ws
|
||||
[package-manager] add -D drizzle-kit dotenv @types/ws
|
||||
```
|
||||
|
||||
## Phase 3: Configuration
|
||||
|
||||
Create configuration files in dependency order:
|
||||
|
||||
### 3.1. Neon Database Provisioning & Environment File
|
||||
|
||||
**Outcome**: A working `.env` or `.env.local` file with a real Neon connection string that the application can use immediately.
|
||||
|
||||
Use MCP tools to list or create a Neon project and get its connection string. Write the actual credentials to the environment file (`.env.local` for Next.js, `.env` for other projects). Add the file to `.gitignore`.
|
||||
|
||||
**Environment file format:**
|
||||
```bash
|
||||
DATABASE_URL=postgresql://user:password@host/database?sslmode=require
|
||||
```
|
||||
|
||||
### 3.2. Drizzle Config
|
||||
|
||||
Create `drizzle.config.ts` with explicit environment loading:
|
||||
|
||||
**CRITICAL:** The `config({ path: '...' })` line must match the environment file from Step 3.1.
|
||||
|
||||
**For Next.js (using .env.local):**
|
||||
```typescript
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
// Load .env.local explicitly
|
||||
config({ path: '.env.local' });
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/db/schema.ts',
|
||||
out: './src/db/migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**For other projects (using .env):**
|
||||
```typescript
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
// Load .env explicitly
|
||||
config({ path: '.env' });
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/db/schema.ts',
|
||||
out: './src/db/migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- Without explicit `config({ path: '...' })`, drizzle-kit may not load environment variables
|
||||
- This prevents "url: undefined" errors during migrations
|
||||
- The path must match your environment file name from Phase 3.1
|
||||
|
||||
### 3.3. Database Connection
|
||||
|
||||
Create `src/db/index.ts` with appropriate adapter (see `references/adapters.md` for decision guide):
|
||||
|
||||
**For Vercel/Edge:**
|
||||
```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);
|
||||
```
|
||||
|
||||
**For Node.js:**
|
||||
```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! });
|
||||
export const db = drizzle(pool);
|
||||
```
|
||||
|
||||
See `templates/db-http.ts` and `templates/db-websocket.ts` for complete examples.
|
||||
|
||||
## Phase 4: Schema Generation
|
||||
|
||||
Based on app type, create appropriate schema:
|
||||
|
||||
### 4.1. Common Patterns
|
||||
|
||||
**Todo App:**
|
||||
```typescript
|
||||
import { pgTable, serial, text, boolean, timestamp, varchar } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
});
|
||||
|
||||
export const todos = pgTable('todos', {
|
||||
id: serial('id').primaryKey(),
|
||||
userId: serial('user_id').notNull().references(() => users.id),
|
||||
title: text('title').notNull(),
|
||||
completed: boolean('completed').default(false),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
});
|
||||
```
|
||||
|
||||
**Blog App:**
|
||||
```typescript
|
||||
import { pgTable, serial, text, timestamp, varchar, index } from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
});
|
||||
|
||||
export const posts = pgTable('posts', {
|
||||
id: serial('id').primaryKey(),
|
||||
userId: serial('user_id').notNull().references(() => users.id),
|
||||
title: text('title').notNull(),
|
||||
content: text('content').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
}, (table) => ({
|
||||
userIdIdx: index('posts_user_id_idx').on(table.userId),
|
||||
}));
|
||||
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
posts: many(posts),
|
||||
}));
|
||||
|
||||
export const postsRelations = relations(posts, ({ one }) => ({
|
||||
author: one(users, {
|
||||
fields: [posts.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
```
|
||||
|
||||
See `templates/schema-example.ts` for more complex examples.
|
||||
|
||||
## Phase 5: Migrations
|
||||
|
||||
Run migrations with proper error handling:
|
||||
|
||||
### 5.1. Generate Migration
|
||||
|
||||
```bash
|
||||
[package-manager] drizzle-kit generate
|
||||
```
|
||||
|
||||
This creates SQL files in `src/db/migrations/`.
|
||||
|
||||
### 5.2. Apply Migration
|
||||
|
||||
**Recommended approach (explicit env loading):**
|
||||
```bash
|
||||
export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \
|
||||
[package-manager] drizzle-kit migrate
|
||||
```
|
||||
|
||||
**Why this works:** Ensures `DATABASE_URL` is available, preventing "url: undefined" errors.
|
||||
|
||||
### 5.3. Add Migration Scripts
|
||||
|
||||
Add these convenience scripts to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
npm run db:generate # Generate migrations from schema changes
|
||||
npm run db:migrate # Apply pending migrations
|
||||
npm run db:push # Push schema directly (dev only)
|
||||
npm run db:studio # Open Drizzle Studio
|
||||
```
|
||||
|
||||
**Note:** Replace `npm run` with your package manager's equivalent (`pnpm`, `yarn`, `bun`).
|
||||
|
||||
### 5.4. If Migration Fails
|
||||
|
||||
See `guides/troubleshooting.md` for common issues and fixes.
|
||||
|
||||
Also reference `references/migrations.md` for deep dive on migration patterns.
|
||||
|
||||
## Phase 6: Add Best Practices References
|
||||
|
||||
Before executing the add-neon-docs skill, provide a summary of everything that has been done:
|
||||
|
||||
"✅ ... Drizzle integration is complete! Now adding documentation references..."
|
||||
|
||||
Then execute the neon-plugin:add-neon-docs skill with the parameter SKILL_NAME="neon-drizzle"
|
||||
|
||||
This will add reference links to Neon + Drizzle best practices documentation in your project's AI documentation file, helping AI assistants provide better guidance in future conversations.
|
||||
|
||||
## ✅ Setup Complete!
|
||||
|
||||
Your Drizzle + Neon integration is ready to use.
|
||||
|
||||
415
skills/neon-drizzle/guides/schema-only.md
Normal file
415
skills/neon-drizzle/guides/schema-only.md
Normal file
@@ -0,0 +1,415 @@
|
||||
# Schema Creation and Modification
|
||||
|
||||
> **Complete Walkthrough**: This is a self-contained, step-by-step guide with its own numbered phases (Phase 1-6).
|
||||
> Follow each phase in order for schema design, modification, and migration workflows.
|
||||
|
||||
Guide for creating or modifying database schemas with Drizzle.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Workflow Checklist](#workflow-checklist)
|
||||
- [Phase 1: Schema Design Patterns](#phase-1-schema-design-patterns)
|
||||
- [Phase 2: Common Schema Patterns](#phase-2-common-schema-patterns)
|
||||
- [Phase 3: Schema Modifications](#phase-3-schema-modifications)
|
||||
- [Phase 4: Indexes and Constraints](#phase-4-indexes-and-constraints)
|
||||
- [Phase 5: Generate and Apply Changes](#phase-5-generate-and-apply-changes)
|
||||
- [Phase 6: Advanced Patterns](#phase-6-advanced-patterns)
|
||||
- [Common Issues](#common-issues)
|
||||
- [Next Steps](#next-steps)
|
||||
|
||||
---
|
||||
|
||||
## Workflow Checklist
|
||||
|
||||
When following this guide, I will track these high-level tasks:
|
||||
|
||||
- [ ] Design schema using appropriate patterns (tables, relationships, types)
|
||||
- [ ] Apply common schema patterns (auth, soft deletes, enums, JSON)
|
||||
- [ ] Implement schema modifications (add/rename/drop columns, change types)
|
||||
- [ ] Add indexes and constraints for performance and data integrity
|
||||
- [ ] Generate and apply migrations
|
||||
- [ ] Verify changes and test with queries
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Schema Design Patterns
|
||||
|
||||
### 1.1. Basic Table Structure
|
||||
|
||||
```typescript
|
||||
import { pgTable, serial, text, varchar, timestamp, boolean } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const tableName = pgTable('table_name', {
|
||||
id: serial('id').primaryKey(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
isActive: boolean('is_active').default(true),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow(),
|
||||
});
|
||||
```
|
||||
|
||||
**Key conventions:**
|
||||
- Use `serial` for auto-incrementing IDs
|
||||
- Use `varchar` for short strings (with length limit)
|
||||
- Use `text` for long strings
|
||||
- Use `timestamp` for dates/times
|
||||
- Always add `createdAt` for audit trails
|
||||
|
||||
### 1.2. Relationships
|
||||
|
||||
**One-to-Many:**
|
||||
```typescript
|
||||
import { pgTable, serial, text, timestamp, index } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const authors = pgTable('authors', {
|
||||
id: serial('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
});
|
||||
|
||||
export const posts = pgTable('posts', {
|
||||
id: serial('id').primaryKey(),
|
||||
authorId: serial('author_id')
|
||||
.notNull()
|
||||
.references(() => authors.id),
|
||||
title: text('title').notNull(),
|
||||
content: text('content').notNull(),
|
||||
}, (table) => ({
|
||||
authorIdIdx: index('posts_author_id_idx').on(table.authorId),
|
||||
}));
|
||||
```
|
||||
|
||||
**Important:** Always add index on foreign keys for query performance.
|
||||
|
||||
**Many-to-Many:**
|
||||
```typescript
|
||||
export const posts = pgTable('posts', {
|
||||
id: serial('id').primaryKey(),
|
||||
title: text('title').notNull(),
|
||||
});
|
||||
|
||||
export const tags = pgTable('tags', {
|
||||
id: serial('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
});
|
||||
|
||||
export const postsTags = pgTable('posts_tags', {
|
||||
postId: serial('post_id')
|
||||
.notNull()
|
||||
.references(() => posts.id),
|
||||
tagId: serial('tag_id')
|
||||
.notNull()
|
||||
.references(() => tags.id),
|
||||
}, (table) => ({
|
||||
pk: index('posts_tags_pk').on(table.postId, table.tagId),
|
||||
}));
|
||||
```
|
||||
|
||||
### 1.3. Type-Safe Relations
|
||||
|
||||
Enable relational queries:
|
||||
```typescript
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
export const authorsRelations = relations(authors, ({ many }) => ({
|
||||
posts: many(posts),
|
||||
}));
|
||||
|
||||
export const postsRelations = relations(posts, ({ one }) => ({
|
||||
author: one(authors, {
|
||||
fields: [posts.authorId],
|
||||
references: [authors.id],
|
||||
}),
|
||||
}));
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Type-safe joins
|
||||
- Automatic loading of related data
|
||||
- No manual JOIN queries needed
|
||||
|
||||
## Phase 2: Common Schema Patterns
|
||||
|
||||
### 2.1. User Authentication
|
||||
|
||||
```typescript
|
||||
import { pgTable, serial, varchar, timestamp, boolean } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
passwordHash: varchar('password_hash', { length: 255 }),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
emailVerified: boolean('email_verified').default(false),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
lastLoginAt: timestamp('last_login_at'),
|
||||
});
|
||||
```
|
||||
|
||||
### 2.2. Soft Deletes
|
||||
|
||||
```typescript
|
||||
export const posts = pgTable('posts', {
|
||||
id: serial('id').primaryKey(),
|
||||
title: text('title').notNull(),
|
||||
content: text('content').notNull(),
|
||||
deletedAt: timestamp('deleted_at'),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
});
|
||||
```
|
||||
|
||||
Query with soft deletes:
|
||||
```typescript
|
||||
const activePosts = await db
|
||||
.select()
|
||||
.from(posts)
|
||||
.where(isNull(posts.deletedAt));
|
||||
```
|
||||
|
||||
### 2.3. Enums
|
||||
|
||||
```typescript
|
||||
import { pgEnum, pgTable, serial, text } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const statusEnum = pgEnum('status', ['draft', 'published', 'archived']);
|
||||
|
||||
export const posts = pgTable('posts', {
|
||||
id: serial('id').primaryKey(),
|
||||
title: text('title').notNull(),
|
||||
status: statusEnum('status').default('draft'),
|
||||
});
|
||||
```
|
||||
|
||||
### 2.4. JSON Fields
|
||||
|
||||
```typescript
|
||||
import { pgTable, serial, jsonb } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const products = pgTable('products', {
|
||||
id: serial('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
metadata: jsonb('metadata').$type<{
|
||||
color?: string;
|
||||
size?: string;
|
||||
tags?: string[];
|
||||
}>(),
|
||||
});
|
||||
```
|
||||
|
||||
## Phase 3: Schema Modifications
|
||||
|
||||
### 3.1. Adding Columns
|
||||
|
||||
**Step 1:** Update schema:
|
||||
```typescript
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull(),
|
||||
phoneNumber: varchar('phone_number', { length: 20 }), // NEW
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2:** Generate migration:
|
||||
```bash
|
||||
[package-manager] drizzle-kit generate
|
||||
```
|
||||
|
||||
**Step 3:** Apply migration:
|
||||
```bash
|
||||
export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \
|
||||
[package-manager] drizzle-kit migrate
|
||||
```
|
||||
|
||||
### 3.2. Renaming Columns
|
||||
|
||||
**Important:** Drizzle sees renames as drop + add. Manual migration required.
|
||||
|
||||
**Step 1:** Update schema:
|
||||
```typescript
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
fullName: varchar('full_name', { length: 255 }), // was 'name'
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2:** Generate migration (will create drop + add):
|
||||
```bash
|
||||
[package-manager] drizzle-kit generate
|
||||
```
|
||||
|
||||
**Step 3:** Edit migration file manually:
|
||||
```sql
|
||||
-- Change from:
|
||||
-- ALTER TABLE users DROP COLUMN name;
|
||||
-- ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
|
||||
|
||||
-- To:
|
||||
ALTER TABLE users RENAME COLUMN name TO full_name;
|
||||
```
|
||||
|
||||
**Step 4:** Apply migration:
|
||||
```bash
|
||||
[package-manager] drizzle-kit migrate
|
||||
```
|
||||
|
||||
### 3.3. Dropping Columns
|
||||
|
||||
**Step 1:** Remove from schema:
|
||||
```typescript
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull(),
|
||||
// removed: phoneNumber
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2:** Generate and apply:
|
||||
```bash
|
||||
[package-manager] drizzle-kit generate
|
||||
[package-manager] drizzle-kit migrate
|
||||
```
|
||||
|
||||
**Warning:** This permanently deletes data. Back up first!
|
||||
|
||||
### 3.4. Changing Column Types
|
||||
|
||||
**Step 1:** Update schema:
|
||||
```typescript
|
||||
export const posts = pgTable('posts', {
|
||||
id: serial('id').primaryKey(),
|
||||
views: bigint('views', { mode: 'number' }), // was: integer
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2:** Generate migration:
|
||||
```bash
|
||||
[package-manager] drizzle-kit generate
|
||||
```
|
||||
|
||||
**Step 3:** Review generated SQL - may need data migration if incompatible types.
|
||||
|
||||
## Phase 4: Indexes and Constraints
|
||||
|
||||
### 4.1. Add Indexes
|
||||
|
||||
**Single column:**
|
||||
```typescript
|
||||
import { pgTable, serial, text, index } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const posts = pgTable('posts', {
|
||||
id: serial('id').primaryKey(),
|
||||
title: text('title').notNull(),
|
||||
authorId: serial('author_id').notNull(),
|
||||
}, (table) => ({
|
||||
titleIdx: index('posts_title_idx').on(table.title),
|
||||
authorIdIdx: index('posts_author_id_idx').on(table.authorId),
|
||||
}));
|
||||
```
|
||||
|
||||
**Composite index:**
|
||||
```typescript
|
||||
export const posts = pgTable('posts', {
|
||||
id: serial('id').primaryKey(),
|
||||
authorId: serial('author_id').notNull(),
|
||||
status: text('status').notNull(),
|
||||
}, (table) => ({
|
||||
authorStatusIdx: index('posts_author_status_idx').on(table.authorId, table.status),
|
||||
}));
|
||||
```
|
||||
|
||||
### 4.2. Unique Constraints
|
||||
|
||||
**Single column:**
|
||||
```typescript
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
});
|
||||
```
|
||||
|
||||
**Multiple columns:**
|
||||
```typescript
|
||||
import { pgTable, serial, text, unique } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const postsTags = pgTable('posts_tags', {
|
||||
postId: serial('post_id').notNull(),
|
||||
tagId: serial('tag_id').notNull(),
|
||||
}, (table) => ({
|
||||
unq: unique('posts_tags_unique').on(table.postId, table.tagId),
|
||||
}));
|
||||
```
|
||||
|
||||
### 4.3. Check Constraints
|
||||
|
||||
```typescript
|
||||
import { pgTable, serial, integer, check } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const products = pgTable('products', {
|
||||
id: serial('id').primaryKey(),
|
||||
price: integer('price').notNull(),
|
||||
discountedPrice: integer('discounted_price'),
|
||||
}, (table) => ({
|
||||
priceCheck: check('price_check', 'price >= 0'),
|
||||
discountCheck: check('discount_check', 'discounted_price < price'),
|
||||
}));
|
||||
```
|
||||
|
||||
## Phase 5: Generate and Apply Changes
|
||||
|
||||
### 5.1. Generate Migration
|
||||
|
||||
After any schema changes:
|
||||
```bash
|
||||
[package-manager] drizzle-kit generate
|
||||
```
|
||||
|
||||
Review generated SQL in `src/db/migrations/`.
|
||||
|
||||
### 5.2. Apply Migration
|
||||
|
||||
With proper environment loading:
|
||||
```bash
|
||||
export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \
|
||||
[package-manager] drizzle-kit migrate
|
||||
```
|
||||
|
||||
Or use the migration script:
|
||||
```bash
|
||||
[package-manager] tsx scripts/run-migration.ts
|
||||
```
|
||||
|
||||
### 5.3. Verify Changes
|
||||
|
||||
**Check in database:**
|
||||
```bash
|
||||
psql $DATABASE_URL -c "\d table_name"
|
||||
```
|
||||
|
||||
**Test with queries:**
|
||||
```typescript
|
||||
import { db } from './src/db';
|
||||
import { tableName } from './src/db/schema';
|
||||
|
||||
const result = await db.select().from(tableName);
|
||||
console.log('Schema works:', result);
|
||||
```
|
||||
|
||||
## Phase 6: Advanced Patterns
|
||||
|
||||
For complex schemas, see:
|
||||
- `templates/schema-example.ts` - Multi-table examples with relations
|
||||
- `references/migrations.md` - Advanced migration patterns
|
||||
|
||||
## Common Issues
|
||||
|
||||
- **Migration conflicts:** See `guides/troubleshooting.md`
|
||||
- **Relationship errors:** Ensure foreign keys reference correct columns
|
||||
- **Type mismatches:** Match TypeScript types with SQL types carefully
|
||||
|
||||
## Next Steps
|
||||
|
||||
After schema creation:
|
||||
1. Run migrations (see above)
|
||||
2. Create queries (see `references/query-patterns.md`)
|
||||
3. Add validation (use Zod or similar)
|
||||
4. Test thoroughly before production
|
||||
539
skills/neon-drizzle/guides/troubleshooting.md
Normal file
539
skills/neon-drizzle/guides/troubleshooting.md
Normal file
@@ -0,0 +1,539 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
> **Reference Guide**: This is organized by error type and solution, not sequential phases.
|
||||
> Jump directly to the error you're experiencing for quick resolution.
|
||||
|
||||
Common issues and solutions for Drizzle ORM with Neon.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Migration Errors](#migration-errors)
|
||||
- [Connection Errors](#connection-errors)
|
||||
- [Adapter Issues](#adapter-issues)
|
||||
- [Type Errors](#type-errors)
|
||||
- [Query Errors](#query-errors)
|
||||
- [Performance Issues](#performance-issues)
|
||||
- [Environment Issues](#environment-issues)
|
||||
- [Getting More Help](#getting-more-help)
|
||||
- [Prevention Checklist](#prevention-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Migration Errors
|
||||
|
||||
### Error: "url: undefined"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: url is undefined in dbCredentials
|
||||
```
|
||||
|
||||
**Cause:** Environment variables not loaded during migration.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
**Option 1: Explicit env loading**
|
||||
```bash
|
||||
export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \
|
||||
[package-manager] drizzle-kit migrate
|
||||
```
|
||||
|
||||
**Option 2: Update 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!,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Option 3: Use programmatic migration**
|
||||
```typescript
|
||||
import { migrate } from 'drizzle-orm/neon-http/migrator';
|
||||
import { db } from './src/db';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
config({ path: '.env.local' });
|
||||
|
||||
await migrate(db, { migrationsFolder: './src/db/migrations' });
|
||||
```
|
||||
|
||||
### Error: "Cannot find migrations folder"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: ENOENT: no such file or directory, scandir './src/db/migrations'
|
||||
```
|
||||
|
||||
**Cause:** Migrations folder doesn't exist yet.
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
mkdir -p src/db/migrations
|
||||
[package-manager] drizzle-kit generate
|
||||
```
|
||||
|
||||
### Error: "Column already exists"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: column "name" of relation "users" already exists
|
||||
```
|
||||
|
||||
**Cause:** Trying to add a column that already exists in the database.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
**Option 1: Skip migration (dev only)**
|
||||
```bash
|
||||
rm src/db/migrations/[latest-migration-file].sql
|
||||
[package-manager] drizzle-kit generate
|
||||
```
|
||||
|
||||
**Option 2: Drop and recreate table (dev only, DATA LOSS)**
|
||||
```bash
|
||||
psql $DATABASE_URL -c "DROP TABLE users CASCADE;"
|
||||
[package-manager] drizzle-kit migrate
|
||||
```
|
||||
|
||||
**Option 3: Manual migration (production)**
|
||||
Edit the migration file to check if column exists:
|
||||
```sql
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS name VARCHAR(255);
|
||||
```
|
||||
|
||||
### Error: "Migration already applied"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: migration has already been applied
|
||||
```
|
||||
|
||||
**Cause:** Drizzle tracks applied migrations. Trying to reapply.
|
||||
|
||||
**Solution:**
|
||||
|
||||
Check migration journal:
|
||||
```bash
|
||||
cat src/db/migrations/meta/_journal.json
|
||||
```
|
||||
|
||||
Remove duplicate entry or regenerate:
|
||||
```bash
|
||||
rm -rf src/db/migrations
|
||||
mkdir src/db/migrations
|
||||
[package-manager] drizzle-kit generate
|
||||
```
|
||||
|
||||
**Warning:** Only do this in development!
|
||||
|
||||
## Connection Errors
|
||||
|
||||
### Error: "Connection refused"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: connect ECONNREFUSED
|
||||
```
|
||||
|
||||
**Causes and Solutions:**
|
||||
|
||||
**1. Wrong DATABASE_URL format**
|
||||
|
||||
Check format:
|
||||
```bash
|
||||
echo $DATABASE_URL
|
||||
```
|
||||
|
||||
Should be:
|
||||
```
|
||||
postgresql://user:password@host.neon.tech/dbname?sslmode=require
|
||||
```
|
||||
|
||||
**2. Missing sslmode**
|
||||
|
||||
Add to DATABASE_URL:
|
||||
```
|
||||
?sslmode=require
|
||||
```
|
||||
|
||||
**3. Firewall/network issue**
|
||||
|
||||
Test connectivity:
|
||||
```bash
|
||||
psql $DATABASE_URL -c "SELECT 1"
|
||||
```
|
||||
|
||||
### Error: "WebSocket connection failed"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: WebSocket connection to 'wss://...' failed
|
||||
```
|
||||
|
||||
**Cause:** Missing WebSocket constructor in Node.js.
|
||||
|
||||
**Solution:**
|
||||
|
||||
Add to your connection file:
|
||||
```typescript
|
||||
import { neonConfig } from '@neondatabase/serverless';
|
||||
import ws from 'ws';
|
||||
|
||||
neonConfig.webSocketConstructor = ws;
|
||||
```
|
||||
|
||||
Install ws if missing:
|
||||
```bash
|
||||
[package-manager] add ws
|
||||
[package-manager] add -D @types/ws
|
||||
```
|
||||
|
||||
### Error: "Too many connections"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: sorry, too many clients already
|
||||
```
|
||||
|
||||
**Cause:** Connection pool exhausted.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
**For HTTP adapter:** This shouldn't happen (stateless).
|
||||
|
||||
**For WebSocket adapter:** Implement connection pooling:
|
||||
```typescript
|
||||
import { Pool } from '@neondatabase/serverless';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL!,
|
||||
max: 10, // Limit connections
|
||||
});
|
||||
|
||||
export const db = drizzle(pool);
|
||||
```
|
||||
|
||||
**Close connections properly:**
|
||||
```typescript
|
||||
process.on('SIGTERM', async () => {
|
||||
await pool.end();
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
## Adapter Issues
|
||||
|
||||
### Wrong Adapter for Environment
|
||||
|
||||
**Symptom:** App works locally but fails in production (or vice versa).
|
||||
|
||||
**Cause:** Using wrong adapter for environment.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
See `references/adapters.md` for decision guide.
|
||||
|
||||
**Quick reference:**
|
||||
- Vercel/Cloudflare/Edge → HTTP adapter
|
||||
- Node.js/Express/Long-lived → WebSocket adapter
|
||||
|
||||
**HTTP adapter:**
|
||||
```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);
|
||||
```
|
||||
|
||||
**WebSocket adapter:**
|
||||
```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! });
|
||||
export const db = drizzle(pool);
|
||||
```
|
||||
|
||||
## Type Errors
|
||||
|
||||
### Error: "Type 'number' is not assignable to type 'string'"
|
||||
|
||||
**Symptom:**
|
||||
```typescript
|
||||
const user = await db.insert(users).values({
|
||||
id: 1, // Error here
|
||||
email: 'test@example.com',
|
||||
});
|
||||
```
|
||||
|
||||
**Cause:** Trying to manually set auto-increment ID.
|
||||
|
||||
**Solution:**
|
||||
|
||||
Remove `id` from insert (it's auto-generated):
|
||||
```typescript
|
||||
const user = await db.insert(users).values({
|
||||
email: 'test@example.com',
|
||||
});
|
||||
```
|
||||
|
||||
### Error: "Property 'xyz' does not exist"
|
||||
|
||||
**Symptom:**
|
||||
```typescript
|
||||
const user = await db.select().from(users);
|
||||
console.log(user[0].nonExistentField); // Error
|
||||
```
|
||||
|
||||
**Cause:** Column not defined in schema.
|
||||
|
||||
**Solution:**
|
||||
|
||||
Add column to schema:
|
||||
```typescript
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
nonExistentField: text('non_existent_field'),
|
||||
});
|
||||
```
|
||||
|
||||
Then regenerate and apply migration.
|
||||
|
||||
## Query Errors
|
||||
|
||||
### Error: "relation does not exist"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: relation "users" does not exist
|
||||
```
|
||||
|
||||
**Cause:** Table not created in database yet.
|
||||
|
||||
**Solution:**
|
||||
|
||||
Run migrations:
|
||||
```bash
|
||||
[package-manager] drizzle-kit generate
|
||||
export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \
|
||||
[package-manager] drizzle-kit migrate
|
||||
```
|
||||
|
||||
### Error: "column does not exist"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: column "email" does not exist
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
|
||||
**1. Schema out of sync with database**
|
||||
|
||||
Regenerate and apply migrations:
|
||||
```bash
|
||||
[package-manager] drizzle-kit generate
|
||||
[package-manager] drizzle-kit migrate
|
||||
```
|
||||
|
||||
**2. Wrong table name in query**
|
||||
|
||||
Check schema definition vs query.
|
||||
|
||||
**3. Case sensitivity**
|
||||
|
||||
PostgreSQL is case-sensitive. Ensure column names match exactly.
|
||||
|
||||
### Error: "Cannot perform transactions with HTTP adapter"
|
||||
|
||||
**Symptom:**
|
||||
```typescript
|
||||
await db.transaction(async (tx) => {
|
||||
// Error: transactions not supported
|
||||
});
|
||||
```
|
||||
|
||||
**Cause:** HTTP adapter doesn't support transactions.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
**Option 1: Switch to WebSocket adapter** (if environment allows)
|
||||
|
||||
See `references/adapters.md`.
|
||||
|
||||
**Option 2: Use batch operations**
|
||||
```typescript
|
||||
await db.batch([
|
||||
db.insert(users).values({ email: 'test1@example.com' }),
|
||||
db.insert(posts).values({ title: 'Test' }),
|
||||
]);
|
||||
```
|
||||
|
||||
**Option 3: Implement application-level rollback**
|
||||
|
||||
Not ideal, but possible for simple cases.
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Slow Queries
|
||||
|
||||
**Symptoms:** Queries taking seconds instead of milliseconds.
|
||||
|
||||
**Diagnose:**
|
||||
|
||||
**1. Missing indexes**
|
||||
|
||||
Check if foreign keys have indexes:
|
||||
```typescript
|
||||
export const posts = pgTable('posts', {
|
||||
id: serial('id').primaryKey(),
|
||||
authorId: serial('author_id').notNull(),
|
||||
}, (table) => ({
|
||||
authorIdIdx: index('posts_author_id_idx').on(table.authorId), // ADD THIS
|
||||
}));
|
||||
```
|
||||
|
||||
**2. N+1 queries**
|
||||
|
||||
Use relations instead of multiple queries:
|
||||
```typescript
|
||||
const postsWithAuthors = await db.query.posts.findMany({
|
||||
with: {
|
||||
author: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**3. Selecting too much data**
|
||||
|
||||
Select only needed columns:
|
||||
```typescript
|
||||
const users = await db.select({
|
||||
id: users.id,
|
||||
email: users.email,
|
||||
}).from(users);
|
||||
```
|
||||
|
||||
### Connection Timeout
|
||||
|
||||
**Symptom:** Queries timeout in production.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
**1. For Vercel:** Ensure using HTTP adapter (see `references/adapters.md`)
|
||||
|
||||
**2. For Node.js:** Implement connection pooling with retry:
|
||||
```typescript
|
||||
import { Pool } from '@neondatabase/serverless';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL!,
|
||||
max: 10,
|
||||
connectionTimeoutMillis: 5000,
|
||||
idleTimeoutMillis: 30000,
|
||||
});
|
||||
```
|
||||
|
||||
**3. Add query timeout:**
|
||||
```typescript
|
||||
const result = await Promise.race([
|
||||
db.select().from(users),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Query timeout')), 5000)
|
||||
),
|
||||
]);
|
||||
```
|
||||
|
||||
## Environment Issues
|
||||
|
||||
### Error: "DATABASE_URL is undefined"
|
||||
|
||||
**Symptom:** App can't find DATABASE_URL.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
**1. Check env file exists:**
|
||||
```bash
|
||||
ls .env .env.local
|
||||
```
|
||||
|
||||
**2. Verify var is set:**
|
||||
```bash
|
||||
grep DATABASE_URL .env.local
|
||||
```
|
||||
|
||||
**3. Load env vars:**
|
||||
```typescript
|
||||
import { config } from 'dotenv';
|
||||
config({ path: '.env.local' });
|
||||
```
|
||||
|
||||
**4. For Next.js:** Use `NEXT_PUBLIC_` prefix if accessing client-side (NOT recommended for DATABASE_URL):
|
||||
```
|
||||
# Don't do this - security risk
|
||||
NEXT_PUBLIC_DATABASE_URL="..."
|
||||
|
||||
# Do this - server-only
|
||||
DATABASE_URL="..."
|
||||
```
|
||||
|
||||
### Error: "Invalid connection string"
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Error: invalid connection string
|
||||
```
|
||||
|
||||
**Cause:** Malformed DATABASE_URL.
|
||||
|
||||
**Check format:**
|
||||
```
|
||||
postgresql://USER:PASSWORD@HOST:PORT/DATABASE?sslmode=require
|
||||
```
|
||||
|
||||
**Common mistakes:**
|
||||
- Missing `postgresql://` prefix
|
||||
- Special characters in password not URL-encoded
|
||||
- Missing `?sslmode=require`
|
||||
|
||||
**Fix special characters:**
|
||||
```bash
|
||||
# If password is "p@ss&word!"
|
||||
# Encode to: p%40ss%26word%21
|
||||
```
|
||||
|
||||
## Getting More Help
|
||||
|
||||
If your issue isn't listed here:
|
||||
|
||||
1. **Check adapter configuration:** `references/adapters.md`
|
||||
2. **Review migration patterns:** `references/migrations.md`
|
||||
3. **Check query syntax:** `references/query-patterns.md`
|
||||
4. **Search Drizzle docs:** https://orm.drizzle.team/docs
|
||||
5. **Check Neon docs:** https://neon.com/docs
|
||||
|
||||
## Prevention Checklist
|
||||
|
||||
Before deploying:
|
||||
|
||||
- [ ] Environment variables properly loaded
|
||||
- [ ] Correct adapter for environment
|
||||
- [ ] Migrations applied successfully
|
||||
- [ ] Indexes on foreign keys
|
||||
- [ ] Connection pooling configured (if Node.js)
|
||||
- [ ] Error handling for database operations
|
||||
- [ ] .env files in .gitignore
|
||||
- [ ] Test queries work in production environment
|
||||
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
|
||||
77
skills/neon-drizzle/scripts/generate-schema.ts
Normal file
77
skills/neon-drizzle/scripts/generate-schema.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Generate Schema Script
|
||||
*
|
||||
* Generates Drizzle migration files based on schema changes.
|
||||
* Run with: npx drizzle-kit generate
|
||||
*
|
||||
* This creates SQL migration files in the migrations directory
|
||||
* based on differences between your schema.ts and the database.
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function generateSchema() {
|
||||
console.log('🔄 Generating Drizzle migrations...\n');
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync('npx drizzle-kit generate');
|
||||
|
||||
if (stdout) {
|
||||
console.log('📝 Generated migrations:');
|
||||
console.log(stdout);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.warn('⚠️ Warnings:');
|
||||
console.warn(stderr);
|
||||
}
|
||||
|
||||
console.log('\n✅ Migration generation complete!');
|
||||
console.log('\n📋 Next steps:');
|
||||
console.log(' 1. Review the generated migration files in ./src/db/migrations');
|
||||
console.log(' 2. Run: npx drizzle-kit migrate');
|
||||
console.log(' 3. Test your application\n');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Migration generation failed');
|
||||
console.error((error as any).message);
|
||||
|
||||
console.log('\n💡 Troubleshooting:');
|
||||
console.log(' • Ensure drizzle.config.ts is in your project root');
|
||||
console.log(' • Check that DATABASE_URL is set correctly');
|
||||
console.log(' • Verify your schema.ts file exists at the configured path');
|
||||
console.log(' • Review guides/troubleshooting.md for common issues');
|
||||
console.log(' • See references/migrations.md for migration patterns');
|
||||
|
||||
const errorMessage = (error as any).message.toLowerCase();
|
||||
|
||||
if (errorMessage.includes('url') || errorMessage.includes('undefined')) {
|
||||
console.log('\n⚠️ Environment variable issue detected:');
|
||||
console.log(' • Ensure DATABASE_URL is loaded in drizzle.config.ts');
|
||||
console.log(' • Add: import { config } from "dotenv"; config({ path: ".env.local" });');
|
||||
console.log(' • See guides/troubleshooting.md section: "Error: url: undefined"');
|
||||
}
|
||||
|
||||
if (errorMessage.includes('schema') || errorMessage.includes('not found')) {
|
||||
console.log('\n⚠️ Schema file issue detected:');
|
||||
console.log(' • Verify schema path in drizzle.config.ts matches actual file location');
|
||||
console.log(' • Default: ./src/db/schema.ts');
|
||||
}
|
||||
|
||||
if (errorMessage.includes('enoent')) {
|
||||
console.log('\n⚠️ File/directory missing:');
|
||||
console.log(' • Create migrations folder: mkdir -p src/db/migrations');
|
||||
console.log(' • Ensure schema file exists: src/db/schema.ts');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
generateSchema().then((success) => {
|
||||
process.exit(success ? 0 : 1);
|
||||
});
|
||||
133
skills/neon-drizzle/scripts/run-migration.ts
Normal file
133
skills/neon-drizzle/scripts/run-migration.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Run Migration Script
|
||||
*
|
||||
* Applies pending Drizzle migrations to your Neon database.
|
||||
* Run with: npx ts-node run-migration.ts
|
||||
*
|
||||
* This script will:
|
||||
* 1. Connect to your Neon database
|
||||
* 2. Apply all pending migrations
|
||||
* 3. Report success or failure
|
||||
*/
|
||||
|
||||
import { drizzle } from 'drizzle-orm/neon-http';
|
||||
import { migrate } from 'drizzle-orm/neon-http/migrator';
|
||||
import { neon } from '@neondatabase/serverless';
|
||||
|
||||
const DATABASE_URL = process.env.DATABASE_URL;
|
||||
|
||||
if (!DATABASE_URL) {
|
||||
console.error('❌ DATABASE_URL environment variable is not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function runMigrations() {
|
||||
console.log('🔄 Running Drizzle migrations...\n');
|
||||
|
||||
try {
|
||||
// Create SQL client
|
||||
const sql = neon(DATABASE_URL);
|
||||
|
||||
// Create Drizzle instance
|
||||
const db = drizzle(sql);
|
||||
|
||||
// Run migrations
|
||||
console.log('⏳ Applying migrations...');
|
||||
await migrate(db, {
|
||||
migrationsFolder: './src/db/migrations',
|
||||
});
|
||||
|
||||
console.log('✅ All migrations applied successfully!\n');
|
||||
|
||||
// Show migration status
|
||||
console.log('📊 Migration Summary:');
|
||||
console.log(' Database: ' + new URL(DATABASE_URL).pathname.slice(1));
|
||||
console.log(' Migrations folder: ./src/db/migrations');
|
||||
console.log(' Status: Up to date\n');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed');
|
||||
console.error((error as any).message);
|
||||
|
||||
console.log('\n💡 Troubleshooting:');
|
||||
console.log(' • Ensure ./src/db/migrations directory exists');
|
||||
console.log(' • Verify DATABASE_URL is correct');
|
||||
console.log(' • Check that migrations are properly formatted SQL files');
|
||||
console.log(' • Try running: npx drizzle-kit generate first');
|
||||
console.log(' • Review guides/troubleshooting.md for common migration errors');
|
||||
console.log(' • See references/migrations.md for detailed migration guide');
|
||||
|
||||
const errorMessage = (error as any).message.toLowerCase();
|
||||
|
||||
if (errorMessage.includes('connect') || errorMessage.includes('connection')) {
|
||||
console.log('\n⚠️ Connection issue detected:');
|
||||
console.log(' • Verify DATABASE_URL format: postgresql://user:pass@host/db?sslmode=require');
|
||||
console.log(' • Ensure database is accessible');
|
||||
console.log(' • Check firewall/network settings');
|
||||
console.log(' • See guides/troubleshooting.md section: "Connection Errors"');
|
||||
}
|
||||
|
||||
if (errorMessage.includes('already exists') || errorMessage.includes('duplicate')) {
|
||||
console.log('\n⚠️ Migration conflict detected:');
|
||||
console.log(' • Migration may have been partially applied');
|
||||
console.log(' • Check database state: psql $DATABASE_URL -c "\\dt"');
|
||||
console.log(' • See references/migrations.md for handling conflicts');
|
||||
}
|
||||
|
||||
if (errorMessage.includes('not found') || errorMessage.includes('enoent')) {
|
||||
console.log('\n⚠️ Migrations folder missing:');
|
||||
console.log(' • Run: npx drizzle-kit generate');
|
||||
console.log(' • Ensure migrations folder path matches drizzle.config.ts');
|
||||
}
|
||||
|
||||
if (errorMessage.includes('syntax')) {
|
||||
console.log('\n⚠️ SQL syntax error:');
|
||||
console.log(' • Review generated migration files in ./src/db/migrations');
|
||||
console.log(' • Check for manually edited migrations');
|
||||
console.log(' • See references/migrations.md for safe editing practices');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative: Run migrations with WebSocket (for Node.js)
|
||||
* Uncomment below if using WebSocket connections
|
||||
*/
|
||||
|
||||
/*
|
||||
import { drizzle } from 'drizzle-orm/neon-serverless';
|
||||
import { migrate } from 'drizzle-orm/neon-serverless/migrator';
|
||||
import { Pool } from '@neondatabase/serverless';
|
||||
|
||||
async function runMigrationsWebSocket() {
|
||||
console.log('🔄 Running Drizzle migrations (WebSocket)...\n');
|
||||
|
||||
const pool = new Pool({ connectionString: DATABASE_URL });
|
||||
|
||||
try {
|
||||
const db = drizzle(pool);
|
||||
|
||||
console.log('⏳ Applying migrations...');
|
||||
await migrate(db, {
|
||||
migrationsFolder: './src/db/migrations',
|
||||
});
|
||||
|
||||
console.log('✅ All migrations applied successfully!\n');
|
||||
await pool.end();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', (error as any).message);
|
||||
await pool.end();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Run migrations
|
||||
runMigrations().then((success) => {
|
||||
process.exit(success ? 0 : 1);
|
||||
});
|
||||
6
skills/neon-drizzle/templates/db-http.ts
Normal file
6
skills/neon-drizzle/templates/db-http.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { drizzle } from 'drizzle-orm/neon-http';
|
||||
import { neon } from '@neondatabase/serverless';
|
||||
|
||||
const sql = neon(process.env.DATABASE_URL!);
|
||||
|
||||
export const db = drizzle(sql);
|
||||
24
skills/neon-drizzle/templates/db-websocket.ts
Normal file
24
skills/neon-drizzle/templates/db-websocket.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await pool.end();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await pool.end();
|
||||
process.exit(0);
|
||||
});
|
||||
96
skills/neon-drizzle/templates/drizzle-config.ts
Normal file
96
skills/neon-drizzle/templates/drizzle-config.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Drizzle Configuration
|
||||
*
|
||||
* This file configures Drizzle ORM for use with Neon.
|
||||
* Place this in your project root or src/ directory.
|
||||
*
|
||||
* Usage: Reference this in your drizzle.config.ts
|
||||
*/
|
||||
|
||||
import { config } from 'dotenv';
|
||||
import type { Config } from 'drizzle-kit';
|
||||
|
||||
config({ path: '.env.local' });
|
||||
|
||||
/**
|
||||
* Drizzle Configuration for Neon Postgres
|
||||
*
|
||||
* Supports both HTTP and WebSocket connections.
|
||||
* Automatically detects which driver to use based on environment.
|
||||
*/
|
||||
|
||||
const dbUrl = process.env.DATABASE_URL;
|
||||
|
||||
if (!dbUrl) {
|
||||
throw new Error('DATABASE_URL environment variable is not set');
|
||||
}
|
||||
|
||||
// Determine connection type based on environment
|
||||
const isServerless = process.env.RUNTIME === 'edge' ||
|
||||
process.env.VERCEL_ENV === 'production';
|
||||
|
||||
export default {
|
||||
schema: './src/db/schema.ts', // Path to your schema file
|
||||
out: './src/db/migrations', // Output directory for migrations
|
||||
|
||||
// Database connection
|
||||
dbCredentials: {
|
||||
url: dbUrl,
|
||||
},
|
||||
|
||||
// Migration options
|
||||
migrations: {
|
||||
prefix: 'timestamp', // or 'none'
|
||||
},
|
||||
|
||||
// Verbose logging for debugging
|
||||
verbose: process.env.DEBUG === 'true',
|
||||
|
||||
// Strict mode ensures all migrations are applied
|
||||
strict: true,
|
||||
} satisfies Config;
|
||||
|
||||
/**
|
||||
* HTTP Connection Configuration (for Vercel Edge, etc.)
|
||||
*
|
||||
* export default {
|
||||
* schema: './src/db/schema.ts',
|
||||
* out: './src/db/migrations',
|
||||
* driver: 'postgres',
|
||||
* dbCredentials: {
|
||||
* url: process.env.DATABASE_URL!,
|
||||
* },
|
||||
* } satisfies Config;
|
||||
*/
|
||||
|
||||
/**
|
||||
* WebSocket Connection Configuration (for Node.js servers)
|
||||
*
|
||||
* export default {
|
||||
* schema: './src/db/schema.ts',
|
||||
* out: './src/db/migrations',
|
||||
* driver: 'pg',
|
||||
* dbCredentials: {
|
||||
* url: process.env.DATABASE_URL!,
|
||||
* },
|
||||
* } satisfies Config;
|
||||
*/
|
||||
|
||||
/**
|
||||
* Migration Commands
|
||||
*
|
||||
* # Generate migration files from schema changes
|
||||
* npx drizzle-kit generate
|
||||
*
|
||||
* # Apply migrations to database
|
||||
* npx drizzle-kit migrate
|
||||
*
|
||||
* # Drop all tables (careful!)
|
||||
* npx drizzle-kit drop
|
||||
*
|
||||
* # Introspect existing database
|
||||
* npx drizzle-kit introspect
|
||||
*
|
||||
* # Push schema changes directly (development only)
|
||||
* npx drizzle-kit push
|
||||
*/
|
||||
231
skills/neon-drizzle/templates/schema-example.ts
Normal file
231
skills/neon-drizzle/templates/schema-example.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Drizzle Schema Example
|
||||
*
|
||||
* This file demonstrates how to define database tables and relationships
|
||||
* using Drizzle ORM with Neon Postgres.
|
||||
*
|
||||
* Usage: Import these tables in your application code for type-safe queries
|
||||
*/
|
||||
|
||||
import {
|
||||
pgTable,
|
||||
serial,
|
||||
text,
|
||||
varchar,
|
||||
integer,
|
||||
timestamp,
|
||||
boolean,
|
||||
decimal,
|
||||
json,
|
||||
index,
|
||||
unique,
|
||||
foreignKey,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
/**
|
||||
* Users Table
|
||||
*
|
||||
* Stores basic user information. Can be extended with additional fields
|
||||
* as needed by your application.
|
||||
*/
|
||||
export const users = pgTable(
|
||||
'users',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
password: text('password'), // If not using external auth
|
||||
avatar: text('avatar'), // URL to avatar image
|
||||
isActive: boolean('is_active').default(true),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
emailIdx: index('users_email_idx').on(table.email),
|
||||
createdAtIdx: index('users_created_at_idx').on(table.createdAt),
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Profiles Table
|
||||
*
|
||||
* Extended user information. Uses a foreign key to link with users.
|
||||
*/
|
||||
export const profiles = pgTable('profiles', {
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
bio: text('bio'),
|
||||
location: varchar('location', { length: 255 }),
|
||||
website: varchar('website', { length: 255 }),
|
||||
phone: varchar('phone', { length: 20 }),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Posts Table
|
||||
*
|
||||
* Blog posts created by users.
|
||||
*/
|
||||
export const posts = pgTable(
|
||||
'posts',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
slug: varchar('slug', { length: 255 }).notNull().unique(),
|
||||
content: text('content').notNull(),
|
||||
excerpt: text('excerpt'),
|
||||
published: boolean('published').default(false),
|
||||
publishedAt: timestamp('published_at'),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
userIdIdx: index('posts_user_id_idx').on(table.userId),
|
||||
publishedIdx: index('posts_published_idx').on(table.published),
|
||||
slugIdx: index('posts_slug_idx').on(table.slug),
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Comments Table
|
||||
*
|
||||
* Comments on blog posts. Supports nested comments via parent_id.
|
||||
*/
|
||||
export const comments = pgTable(
|
||||
'comments',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
postId: integer('post_id')
|
||||
.notNull()
|
||||
.references(() => posts.id, { onDelete: 'cascade' }),
|
||||
userId: integer('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
parentId: integer('parent_id').references(() => comments.id, {
|
||||
onDelete: 'cascade',
|
||||
}),
|
||||
content: text('content').notNull(),
|
||||
approved: boolean('approved').default(false),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
postIdIdx: index('comments_post_id_idx').on(table.postId),
|
||||
userIdIdx: index('comments_user_id_idx').on(table.userId),
|
||||
parentIdIdx: index('comments_parent_id_idx').on(table.parentId),
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Tags Table
|
||||
*
|
||||
* Tags for categorizing posts.
|
||||
*/
|
||||
export const tags = pgTable('tags', {
|
||||
id: serial('id').primaryKey(),
|
||||
name: varchar('name', { length: 100 }).notNull().unique(),
|
||||
slug: varchar('slug', { length: 100 }).notNull().unique(),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
});
|
||||
|
||||
/**
|
||||
* PostTags Junction Table
|
||||
*
|
||||
* Many-to-many relationship between posts and tags.
|
||||
*/
|
||||
export const postTags = pgTable(
|
||||
'post_tags',
|
||||
{
|
||||
postId: integer('post_id')
|
||||
.notNull()
|
||||
.references(() => posts.id, { onDelete: 'cascade' }),
|
||||
tagId: integer('tag_id')
|
||||
.notNull()
|
||||
.references(() => tags.id, { onDelete: 'cascade' }),
|
||||
},
|
||||
(table) => ({
|
||||
pk: { name: 'post_tags_pk', columns: [table.postId, table.tagId] },
|
||||
postIdIdx: index('post_tags_post_id_idx').on(table.postId),
|
||||
tagIdIdx: index('post_tags_tag_id_idx').on(table.tagId),
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Settings Table
|
||||
*
|
||||
* Application-wide or user-specific settings stored as JSON.
|
||||
*/
|
||||
export const settings = pgTable('settings', {
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id').references(() => users.id, {
|
||||
onDelete: 'cascade',
|
||||
}), // null = global settings
|
||||
key: varchar('key', { length: 255 }).notNull(),
|
||||
value: json('value'),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow(),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Relations (optional but recommended for better type safety)
|
||||
// ============================================================================
|
||||
|
||||
export const usersRelations = relations(users, ({ many, one }) => ({
|
||||
profile: one(profiles),
|
||||
posts: many(posts),
|
||||
comments: many(comments),
|
||||
}));
|
||||
|
||||
export const profilesRelations = relations(profiles, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [profiles.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const postsRelations = relations(posts, ({ one, many }) => ({
|
||||
author: one(users, {
|
||||
fields: [posts.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
comments: many(comments),
|
||||
tags: many(postTags),
|
||||
}));
|
||||
|
||||
export const commentsRelations = relations(comments, ({ one, many }) => ({
|
||||
post: one(posts, {
|
||||
fields: [comments.postId],
|
||||
references: [posts.id],
|
||||
}),
|
||||
author: one(users, {
|
||||
fields: [comments.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
parent: one(comments, {
|
||||
fields: [comments.parentId],
|
||||
references: [comments.id],
|
||||
}),
|
||||
replies: many(comments),
|
||||
}));
|
||||
|
||||
export const tagsRelations = relations(tags, ({ many }) => ({
|
||||
posts: many(postTags),
|
||||
}));
|
||||
|
||||
export const postTagsRelations = relations(postTags, ({ one }) => ({
|
||||
post: one(posts, {
|
||||
fields: [postTags.postId],
|
||||
references: [posts.id],
|
||||
}),
|
||||
tag: one(tags, {
|
||||
fields: [postTags.tagId],
|
||||
references: [tags.id],
|
||||
}),
|
||||
}));
|
||||
Reference in New Issue
Block a user