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