@raftlabs/raftstack 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/backend/SKILL.md +802 -0
- package/.claude/skills/code-quality/SKILL.md +318 -0
- package/.claude/skills/database/SKILL.md +465 -0
- package/.claude/skills/react/SKILL.md +418 -0
- package/.claude/skills/seo/SKILL.md +446 -0
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2009 -0
- package/dist/cli.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: database
|
|
3
|
+
description: Use when designing database schemas, writing Drizzle ORM code, creating tables, choosing indexes, or when queries are slow and indexes might be missing
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Database Development
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Every foreign key needs an index. Every frequent filter needs an index. Use Drizzle ORM patterns correctly and think about query patterns before defining tables.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- Designing new database schemas
|
|
15
|
+
- Adding tables with Drizzle ORM
|
|
16
|
+
- Optimizing slow queries
|
|
17
|
+
- Choosing between UUID vs serial IDs
|
|
18
|
+
- Deciding index strategy
|
|
19
|
+
|
|
20
|
+
## The Iron Rules
|
|
21
|
+
|
|
22
|
+
### 1. Every Foreign Key Gets an Index
|
|
23
|
+
|
|
24
|
+
Foreign keys don't automatically create indexes in PostgreSQL. You MUST add them.
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// ❌ BAD: FK without index
|
|
28
|
+
export const orders = pgTable("orders", {
|
|
29
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
30
|
+
userId: uuid("user_id").references(() => users.id), // No index!
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ✅ GOOD: FK with explicit index
|
|
34
|
+
export const orders = pgTable("orders", {
|
|
35
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
36
|
+
userId: uuid("user_id").notNull().references(() => users.id),
|
|
37
|
+
}, (table) => ({
|
|
38
|
+
userIdIdx: index("orders_user_id_idx").on(table.userId),
|
|
39
|
+
}));
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Index Frequently Filtered Columns
|
|
43
|
+
|
|
44
|
+
If you WHERE on it, index it.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// ✅ GOOD: Index on status and timestamps
|
|
48
|
+
export const orders = pgTable("orders", {
|
|
49
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
50
|
+
userId: uuid("user_id").notNull().references(() => users.id),
|
|
51
|
+
status: orderStatusEnum("status").default("pending").notNull(),
|
|
52
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
53
|
+
}, (table) => ({
|
|
54
|
+
userIdIdx: index("orders_user_id_idx").on(table.userId),
|
|
55
|
+
statusIdx: index("orders_status_idx").on(table.status),
|
|
56
|
+
createdAtIdx: index("orders_created_at_idx").on(table.createdAt),
|
|
57
|
+
// Composite for common query: "user's orders by status"
|
|
58
|
+
userStatusIdx: index("orders_user_status_idx").on(table.userId, table.status),
|
|
59
|
+
}));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Use Partial Indexes for Filtered Subsets
|
|
63
|
+
|
|
64
|
+
When you frequently query a subset, use a partial index.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// ✅ GOOD: Partial index for active products only
|
|
68
|
+
export const products = pgTable("products", {
|
|
69
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
70
|
+
name: varchar("name", { length: 255 }).notNull(),
|
|
71
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
72
|
+
isFeatured: boolean("is_featured").default(false).notNull(),
|
|
73
|
+
}, (table) => ({
|
|
74
|
+
// Only index active products - smaller, faster
|
|
75
|
+
activeIdx: index("products_active_idx")
|
|
76
|
+
.on(table.isActive)
|
|
77
|
+
.where(sql`${table.isActive} = true`),
|
|
78
|
+
// Featured products (active AND featured)
|
|
79
|
+
featuredIdx: index("products_featured_idx")
|
|
80
|
+
.on(table.isFeatured)
|
|
81
|
+
.where(sql`${table.isActive} = true AND ${table.isFeatured} = true`),
|
|
82
|
+
}));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 4. JSONB with GIN Index for JSON Columns
|
|
86
|
+
|
|
87
|
+
Never store JSON as text. Use JSONB with GIN index for queries.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { jsonb } from "drizzle-orm/pg-core";
|
|
91
|
+
|
|
92
|
+
// ❌ BAD: JSON as text
|
|
93
|
+
imageUrls: text("image_urls"), // Can't query efficiently
|
|
94
|
+
|
|
95
|
+
// ✅ GOOD: JSONB with GIN index
|
|
96
|
+
export const products = pgTable("products", {
|
|
97
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
98
|
+
metadata: jsonb("metadata").$type<ProductMetadata>(),
|
|
99
|
+
tags: jsonb("tags").$type<string[]>().default([]),
|
|
100
|
+
}, (table) => ({
|
|
101
|
+
tagsGinIdx: index("products_tags_gin_idx")
|
|
102
|
+
.using("gin", table.tags),
|
|
103
|
+
}));
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 5. Auto-Update Timestamps
|
|
107
|
+
|
|
108
|
+
Use `$onUpdate` for automatic timestamp updates.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// ✅ GOOD: Auto-updating timestamps
|
|
112
|
+
export const users = pgTable("users", {
|
|
113
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
114
|
+
email: varchar("email", { length: 255 }).notNull().unique(),
|
|
115
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
116
|
+
updatedAt: timestamp("updated_at")
|
|
117
|
+
.defaultNow()
|
|
118
|
+
.notNull()
|
|
119
|
+
.$onUpdate(() => new Date()),
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 6. Primary Key Strategy
|
|
124
|
+
|
|
125
|
+
| Strategy | Use When | Pros | Cons |
|
|
126
|
+
|----------|----------|------|------|
|
|
127
|
+
| **UUID** | Distributed systems, public IDs | No collisions, hide growth | Larger, slower indexes |
|
|
128
|
+
| **Serial/bigserial** | Single DB, internal IDs | Compact, fast | Exposes growth, sequence issues |
|
|
129
|
+
| **ULID/NanoID** | Need sortability + randomness | Sortable, compact | Custom generation |
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// UUID (default recommendation)
|
|
133
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
134
|
+
|
|
135
|
+
// Serial for internal/analytics tables
|
|
136
|
+
id: serial("id").primaryKey(),
|
|
137
|
+
|
|
138
|
+
// ULID for sortable + random
|
|
139
|
+
id: varchar("id", { length: 26 }).primaryKey().$default(() => ulid()),
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Migration Strategy
|
|
143
|
+
|
|
144
|
+
### Generate vs Push
|
|
145
|
+
|
|
146
|
+
| Command | Use When | Output |
|
|
147
|
+
|---------|----------|--------|
|
|
148
|
+
| `drizzle-kit generate` | Production, team environments | SQL migration files + snapshots |
|
|
149
|
+
| `drizzle-kit migrate` | Apply versioned migrations | Executes generated SQL files |
|
|
150
|
+
| `drizzle-kit push` | Local dev, rapid prototyping | Direct schema push, no files |
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Production workflow: versioned migrations
|
|
154
|
+
npx drizzle-kit generate # Creates migrations/0001_*.sql
|
|
155
|
+
npx drizzle-kit migrate # Applies to database
|
|
156
|
+
|
|
157
|
+
# Dev workflow: quick iteration
|
|
158
|
+
npx drizzle-kit push # Direct push, no migration files
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Always use `generate` + `migrate` for:**
|
|
162
|
+
- Production databases
|
|
163
|
+
- Team collaboration
|
|
164
|
+
- Audit trail requirements
|
|
165
|
+
- Rollback capability
|
|
166
|
+
|
|
167
|
+
### Migration Files Structure
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
migrations/
|
|
171
|
+
├── 0001_init.sql # First migration
|
|
172
|
+
├── 0002_add_orders_table.sql # Sequential
|
|
173
|
+
├── meta/
|
|
174
|
+
│ ├── 0001_snapshot.json # Schema snapshots
|
|
175
|
+
│ └── 0002_snapshot.json
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Relations & Nested Queries
|
|
179
|
+
|
|
180
|
+
### Define Relations in Schema
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { relations } from 'drizzle-orm';
|
|
184
|
+
|
|
185
|
+
export const users = pgTable('users', {
|
|
186
|
+
id: uuid('id').defaultRandom().primaryKey(),
|
|
187
|
+
name: varchar('name', { length: 255 }).notNull(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export const posts = pgTable('posts', {
|
|
191
|
+
id: uuid('id').defaultRandom().primaryKey(),
|
|
192
|
+
userId: uuid('user_id').notNull().references(() => users.id),
|
|
193
|
+
title: varchar('title', { length: 255 }).notNull(),
|
|
194
|
+
}, (table) => ({
|
|
195
|
+
userIdIdx: index('posts_user_id_idx').on(table.userId),
|
|
196
|
+
}));
|
|
197
|
+
|
|
198
|
+
// Define relations
|
|
199
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
200
|
+
posts: many(posts),
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
export const postsRelations = relations(posts, ({ one }) => ({
|
|
204
|
+
user: one(users, { fields: [posts.userId], references: [users.id] }),
|
|
205
|
+
}));
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Query with Relations (Avoids N+1)
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// ✅ GOOD: Single query with join
|
|
212
|
+
const usersWithPosts = await db.query.users.findMany({
|
|
213
|
+
with: {
|
|
214
|
+
posts: true, // Eagerly loaded
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ✅ GOOD: Nested relations
|
|
219
|
+
const usersWithPostsAndComments = await db.query.users.findMany({
|
|
220
|
+
with: {
|
|
221
|
+
posts: {
|
|
222
|
+
with: {
|
|
223
|
+
comments: true,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ✅ GOOD: Filtered nested queries
|
|
230
|
+
const activeUsersWithRecentPosts = await db.query.users.findMany({
|
|
231
|
+
where: eq(users.status, 'active'),
|
|
232
|
+
with: {
|
|
233
|
+
posts: {
|
|
234
|
+
where: gte(posts.createdAt, new Date('2024-01-01')),
|
|
235
|
+
limit: 10,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ❌ BAD: N+1 query problem
|
|
241
|
+
const users = await db.select().from(users);
|
|
242
|
+
for (const user of users) {
|
|
243
|
+
user.posts = await db.select().from(posts).where(eq(posts.userId, user.id));
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Prepared Statements
|
|
248
|
+
|
|
249
|
+
Use prepared statements for repeated queries to cache query plans.
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// ✅ GOOD: Prepared statement with placeholders
|
|
253
|
+
const getUserWithPosts = db.query.users.findFirst({
|
|
254
|
+
where: eq(users.id, sql.placeholder('userId')),
|
|
255
|
+
with: {
|
|
256
|
+
posts: {
|
|
257
|
+
where: eq(posts.status, sql.placeholder('status')),
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
}).prepare('get_user_posts'); // Name required for PostgreSQL
|
|
261
|
+
|
|
262
|
+
// Execute multiple times
|
|
263
|
+
const user1 = await getUserWithPosts.execute({ userId: '123', status: 'published' });
|
|
264
|
+
const user2 = await getUserWithPosts.execute({ userId: '456', status: 'published' });
|
|
265
|
+
|
|
266
|
+
// ✅ GOOD: Simple prepared query
|
|
267
|
+
const getOrdersByUser = db
|
|
268
|
+
.select()
|
|
269
|
+
.from(orders)
|
|
270
|
+
.where(eq(orders.userId, sql.placeholder('userId')))
|
|
271
|
+
.prepare('orders_by_user');
|
|
272
|
+
|
|
273
|
+
const orders = await getOrdersByUser.execute({ userId: '123' });
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**When to use:**
|
|
277
|
+
- High-traffic endpoints
|
|
278
|
+
- Repeated queries with different params
|
|
279
|
+
- Query optimization critical paths
|
|
280
|
+
|
|
281
|
+
## Serverless Connection Pooling
|
|
282
|
+
|
|
283
|
+
### Neon Serverless
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { Pool } from '@neondatabase/serverless';
|
|
287
|
+
import { drizzle } from 'drizzle-orm/neon-serverless';
|
|
288
|
+
|
|
289
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
290
|
+
const db = drizzle({ client: pool });
|
|
291
|
+
|
|
292
|
+
// For Node.js with WebSockets
|
|
293
|
+
import ws from 'ws';
|
|
294
|
+
import { neonConfig } from '@neondatabase/serverless';
|
|
295
|
+
|
|
296
|
+
neonConfig.webSocketConstructor = ws;
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Vercel Postgres
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { drizzle } from 'drizzle-orm/vercel-postgres';
|
|
303
|
+
|
|
304
|
+
const db = drizzle(); // Automatically uses POSTGRES_URL from env
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Edge Runtime (HTTP-based)
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { neon } from '@neondatabase/serverless';
|
|
311
|
+
import { drizzle } from 'drizzle-orm/neon-http';
|
|
312
|
+
|
|
313
|
+
const sql = neon(process.env.DATABASE_URL);
|
|
314
|
+
const db = drizzle({ client: sql });
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Key considerations:**
|
|
318
|
+
- HTTP drivers for edge: no TCP connections
|
|
319
|
+
- Pool for serverless: handles connection lifecycle
|
|
320
|
+
- WebSocket for Node: better performance
|
|
321
|
+
|
|
322
|
+
## Quick Reference: Index Types
|
|
323
|
+
|
|
324
|
+
| Index Type | Use For | Example |
|
|
325
|
+
|------------|---------|---------|
|
|
326
|
+
| **B-tree** (default) | Equality, range, ORDER BY | `WHERE status = 'active'`, `ORDER BY created_at` |
|
|
327
|
+
| **GIN** | Arrays, JSONB, full-text | `WHERE tags @> '["sale"]'` |
|
|
328
|
+
| **BRIN** | Large sorted tables (logs) | Time-series data ordered by timestamp |
|
|
329
|
+
| **Hash** | Only exact equality | `WHERE id = 'abc'` (rare use) |
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// GIN for JSONB
|
|
333
|
+
index("idx").using("gin", table.tags)
|
|
334
|
+
|
|
335
|
+
// BRIN for time-series
|
|
336
|
+
index("idx").using("brin", table.createdAt)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Schema Checklist
|
|
340
|
+
|
|
341
|
+
Before committing a schema:
|
|
342
|
+
- [ ] Every foreign key has an index
|
|
343
|
+
- [ ] Frequently filtered columns are indexed
|
|
344
|
+
- [ ] Composite indexes match common query patterns
|
|
345
|
+
- [ ] JSON columns use JSONB, not text
|
|
346
|
+
- [ ] `updatedAt` has `$onUpdate`
|
|
347
|
+
- [ ] Considered partial indexes for subsets
|
|
348
|
+
- [ ] Timestamps indexed if used in WHERE/ORDER BY
|
|
349
|
+
- [ ] Relations defined for nested queries
|
|
350
|
+
- [ ] Migration generated (not just pushed)
|
|
351
|
+
|
|
352
|
+
## Testing Strategy
|
|
353
|
+
|
|
354
|
+
| What to Test | How |
|
|
355
|
+
|--------------|-----|
|
|
356
|
+
| Migrations | Apply to test DB, verify schema |
|
|
357
|
+
| Query performance | Use EXPLAIN ANALYZE |
|
|
358
|
+
| Index usage | Check query plans for index scans |
|
|
359
|
+
| Relations | Verify single query, not N+1 |
|
|
360
|
+
| Prepared statements | Benchmark vs regular queries |
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// Test migration applies cleanly
|
|
364
|
+
import { migrate } from 'drizzle-orm/neon-serverless/migrator';
|
|
365
|
+
|
|
366
|
+
test('migrations apply successfully', async () => {
|
|
367
|
+
await migrate(db, { migrationsFolder: './migrations' });
|
|
368
|
+
// Verify tables exist
|
|
369
|
+
const result = await db.execute(sql`SELECT tablename FROM pg_tables WHERE schemaname='public'`);
|
|
370
|
+
expect(result.rows).toContainEqual({ tablename: 'users' });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Test index usage with EXPLAIN
|
|
374
|
+
test('query uses index for user lookup', async () => {
|
|
375
|
+
const plan = await db.execute(sql`
|
|
376
|
+
EXPLAIN (FORMAT JSON)
|
|
377
|
+
SELECT * FROM orders WHERE user_id = '123'
|
|
378
|
+
`);
|
|
379
|
+
const planText = JSON.stringify(plan.rows);
|
|
380
|
+
expect(planText).toContain('Index Scan'); // Not Seq Scan
|
|
381
|
+
expect(planText).toContain('orders_user_id_idx');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Test relations avoid N+1
|
|
385
|
+
test('users with posts executes single query', async () => {
|
|
386
|
+
const startTime = Date.now();
|
|
387
|
+
const users = await db.query.users.findMany({
|
|
388
|
+
with: { posts: true },
|
|
389
|
+
});
|
|
390
|
+
const duration = Date.now() - startTime;
|
|
391
|
+
|
|
392
|
+
expect(users).toHaveLength(100);
|
|
393
|
+
expect(users[0].posts).toBeDefined();
|
|
394
|
+
expect(duration).toBeLessThan(100); // Single query is fast
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Performance Validation Tools
|
|
399
|
+
|
|
400
|
+
### EXPLAIN ANALYZE
|
|
401
|
+
|
|
402
|
+
```sql
|
|
403
|
+
-- Check if index is used
|
|
404
|
+
EXPLAIN ANALYZE
|
|
405
|
+
SELECT * FROM orders
|
|
406
|
+
WHERE user_id = 'abc' AND status = 'pending';
|
|
407
|
+
|
|
408
|
+
-- Look for:
|
|
409
|
+
-- ✅ Index Scan using orders_user_status_idx
|
|
410
|
+
-- ❌ Seq Scan on orders (missing index!)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Query Metrics
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// Log slow queries in production
|
|
417
|
+
import { sql } from 'drizzle-orm';
|
|
418
|
+
|
|
419
|
+
const startTime = Date.now();
|
|
420
|
+
const result = await db.query.users.findMany({ with: { posts: true } });
|
|
421
|
+
const duration = Date.now() - startTime;
|
|
422
|
+
|
|
423
|
+
if (duration > 100) {
|
|
424
|
+
console.warn(`Slow query: ${duration}ms`, { query: 'users.findMany' });
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## References
|
|
429
|
+
|
|
430
|
+
- [Drizzle ORM Documentation](https://orm.drizzle.team/docs/overview) - Relations, prepared statements, migrations
|
|
431
|
+
- [PostgreSQL Indexes](https://www.postgresql.org/docs/current/indexes.html) - Index types and usage
|
|
432
|
+
- [Drizzle Kit](https://orm.drizzle.team/docs/kit-overview) - Migration management
|
|
433
|
+
|
|
434
|
+
**Version Notes:**
|
|
435
|
+
- Drizzle ORM 0.30+: Improved relations API
|
|
436
|
+
- Drizzle Kit 0.20+: Enhanced migration generation
|
|
437
|
+
|
|
438
|
+
## Red Flags - STOP and Add Indexes
|
|
439
|
+
|
|
440
|
+
| Thought | Reality |
|
|
441
|
+
|---------|---------|
|
|
442
|
+
| "I'll add indexes later if queries are slow" | Add them now. FK indexes are mandatory. |
|
|
443
|
+
| "Indexes slow down writes" | Reads outnumber writes 100:1. Index it. |
|
|
444
|
+
| "The table is small" | Tables grow. Index from day one. |
|
|
445
|
+
| "I don't know the query patterns yet" | FK indexes are always needed. Start there. |
|
|
446
|
+
| "JSON as text is fine for now" | JSONB costs nothing extra. Use it. |
|
|
447
|
+
| "I'll just push for now" | Use generate for prod. You need migration history. |
|
|
448
|
+
| "Relations are overhead" | They prevent N+1 and generate optimal queries. |
|
|
449
|
+
| "Prepared statements are premature" | They're free performance for repeated queries. |
|
|
450
|
+
| "I'll fetch users then their posts" | N+1 problem. Use `with` for single query. |
|
|
451
|
+
|
|
452
|
+
## Common Mistakes
|
|
453
|
+
|
|
454
|
+
| Mistake | Fix |
|
|
455
|
+
|---------|-----|
|
|
456
|
+
| FK without index | Add `.on(table.foreignKeyColumn)` |
|
|
457
|
+
| JSON as text | Use `jsonb()` with GIN index |
|
|
458
|
+
| No composite index | Add for common multi-column filters |
|
|
459
|
+
| Missing $onUpdate | Add for `updatedAt` columns |
|
|
460
|
+
| Unique without index awareness | `unique()` creates index, but document intent |
|
|
461
|
+
| No partial index | Use `where()` for subset queries |
|
|
462
|
+
| Using push in production | Use `drizzle-kit generate` + `migrate` for versioning |
|
|
463
|
+
| N+1 queries with loops | Define relations, use `with` for nested data |
|
|
464
|
+
| No prepared statements | Use `.prepare()` for repeated queries |
|
|
465
|
+
| HTTP driver in Node serverless | Use Pool with WebSockets for better performance |
|