Skip to content

Commit c2e79fb

Browse files
committed
fix(skills): harden curated database guidance
1 parent 70298da commit c2e79fb

8 files changed

Lines changed: 469 additions & 202 deletions

File tree

skills/drizzle-orm/SKILL.md

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ npm install mysql2 # MySQL
2323
npm install better-sqlite3 # SQLite
2424

2525
# Drizzle Kit (migrations)
26-
npm install -D drizzle-kit
26+
npm install -D drizzle-kit@0.31.5
2727
```
2828

2929
### Basic Setup
@@ -100,6 +100,7 @@ export const users = pgTable('users', {
100100
role: text('role', { enum: ['admin', 'user', 'guest'] }).default('user'),
101101
metadata: json('metadata').$type<{ theme: string; locale: string }>(),
102102
isActive: boolean('is_active').default(true),
103+
deletedAt: timestamp('deleted_at'),
103104
createdAt: timestamp('created_at').defaultNow().notNull(),
104105
updatedAt: timestamp('updated_at').defaultNow().notNull(),
105106
}, (table) => ({
@@ -277,16 +278,24 @@ await db.transaction(async (tx) => {
277278
// If any query fails, entire transaction rolls back
278279
});
279280

280-
// Manual control
281-
const tx = db.transaction(async (tx) => {
282-
const user = await tx.insert(users).values({ ... }).returning();
281+
// Manual rollback when a required insert does not return a row
282+
const transactionResult = await db.transaction(async (tx) => {
283+
const [user] = await tx.insert(users).values({
284+
email: 'transactional@example.com',
285+
name: 'Transactional User',
286+
}).returning();
283287

284288
if (!user) {
285289
tx.rollback();
286290
return;
287291
}
288292

289-
await tx.insert(posts).values({ authorId: user.id });
293+
await tx.insert(posts).values({
294+
title: 'Transactional Post',
295+
authorId: 1, // Existing authors.id
296+
});
297+
298+
return user;
290299
});
291300
```
292301

@@ -312,19 +321,19 @@ export default {
312321

313322
```bash
314323
# Generate migration
315-
npx drizzle-kit generate
324+
npx drizzle-kit@0.31.5 generate
316325

317326
# View SQL
318327
cat drizzle/0000_migration.sql
319328

320329
# Apply migration
321-
npx drizzle-kit migrate
330+
npx drizzle-kit@0.31.5 migrate
322331

323332
# Introspect existing database
324-
npx drizzle-kit introspect
333+
npx drizzle-kit@0.31.5 introspect
325334

326335
# Drizzle Studio (database GUI)
327-
npx drizzle-kit studio
336+
npx drizzle-kit@0.31.5 studio
328337
```
329338

330339
### Example Migration

skills/drizzle-orm/references/advanced-schemas.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,25 @@ export const users = pgTable('users', {
1717
role: roleEnum('role').default('user'),
1818
});
1919

20-
// MySQL/SQLite: Use text with constraints
21-
import { mysqlTable, text } from 'drizzle-orm/mysql-core';
20+
// MySQL: enforce the allowed values in the database schema
21+
import { mysqlEnum, mysqlTable } from 'drizzle-orm/mysql-core';
2222

2323
export const users = mysqlTable('users', {
24-
role: text('role', { enum: ['admin', 'user', 'guest'] }).default('user'),
24+
role: mysqlEnum('role', ['admin', 'user', 'guest']).default('user'),
2525
});
26+
27+
// SQLite alternative: text enum metadata is TypeScript-only, so add a DB check.
28+
import { check, sqliteTable, text } from 'drizzle-orm/sqlite-core';
29+
import { sql } from 'drizzle-orm';
30+
31+
export const sqliteUsers = sqliteTable('users', {
32+
role: text('role').notNull().default('user'),
33+
}, (table) => [
34+
check(
35+
'users_role_check',
36+
sql`${table.role} IN ('admin', 'user', 'guest')`,
37+
),
38+
]);
2639
```
2740

2841
### Custom JSON Types
@@ -200,8 +213,15 @@ CREATE POLICY tenant_isolation ON documents
200213
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
201214
*/
202215

203-
// Set tenant context
204-
await db.execute(sql`SET app.current_tenant_id = ${tenantId}`);
216+
// Set tenant context and run every tenant-scoped query on the same transaction.
217+
// `true` makes set_config transaction-local, so pooled connections cannot retain it.
218+
const tenantDocuments = await db.transaction(async (tx) => {
219+
await tx.execute(
220+
sql`SELECT set_config('app.current_tenant_id', ${tenantId}, true)`,
221+
);
222+
223+
return tx.select().from(documents);
224+
});
205225
```
206226

207227
### Schema-Per-Tenant

skills/drizzle-orm/references/performance.md

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ process.on('exit', () => sqlite.close());
8282

8383
```typescript
8484
// ❌ Bad: Fetch all columns
85-
const users = await db.select().from(users);
85+
const allUsers = await db.select().from(users);
8686

8787
// ✅ Good: Fetch only needed columns
88-
const users = await db.select({
88+
const userSummaries = await db.select({
8989
id: users.id,
9090
email: users.email,
9191
name: users.name,
@@ -172,9 +172,9 @@ export default {
172172
async fetch(request: Request, env: Env): Promise<Response> {
173173
const db = drizzle(env.DB);
174174

175-
const users = await db.select().from(users).limit(10);
175+
const edgeUsers = await db.select().from(users).limit(10);
176176

177-
return Response.json(users);
177+
return Response.json(edgeUsers);
178178
},
179179
};
180180
```
@@ -191,9 +191,9 @@ export async function GET() {
191191
const sql = neon(process.env.DATABASE_URL!);
192192
const db = drizzle(sql);
193193

194-
const users = await db.select().from(users);
194+
const edgeUsers = await db.select().from(users);
195195

196-
return Response.json(users);
196+
return Response.json(edgeUsers);
197197
}
198198
```
199199

@@ -267,7 +267,7 @@ async function getCachedData<T>(
267267
}
268268

269269
// Usage
270-
const users = await getCachedData(
270+
const cachedUsers = await getCachedData(
271271
'users:all',
272272
() => db.select().from(users),
273273
600
@@ -283,8 +283,8 @@ CREATE MATERIALIZED VIEW user_stats AS
283283
SELECT
284284
u.id,
285285
u.name,
286-
COUNT(p.id) AS post_count,
287-
COUNT(c.id) AS comment_count
286+
COUNT(DISTINCT p.id) AS post_count,
287+
COUNT(DISTINCT c.id) AS comment_count
288288
FROM users u
289289
LEFT JOIN posts p ON p.author_id = u.id
290290
LEFT JOIN comments c ON c.user_id = u.id
@@ -298,8 +298,8 @@ export const userStats = pgMaterializedView('user_stats').as((qb) =>
298298
qb.select({
299299
id: users.id,
300300
name: users.name,
301-
postCount: sql<number>`COUNT(${posts.id})`,
302-
commentCount: sql<number>`COUNT(${comments.id})`,
301+
postCount: sql<number>`COUNT(DISTINCT ${posts.id})`,
302+
commentCount: sql<number>`COUNT(DISTINCT ${comments.id})`,
303303
})
304304
.from(users)
305305
.leftJoin(posts, eq(posts.authorId, users.id))
@@ -323,17 +323,29 @@ import { copyFrom } from 'pg-copy-streams';
323323
import { pipeline } from 'stream/promises';
324324
import { Readable } from 'stream';
325325

326-
async function bulkInsert(data: any[]) {
326+
function encodeCsvField(value: string): string {
327+
if (value.includes('\0')) {
328+
throw new Error('COPY CSV fields cannot contain NUL bytes');
329+
}
330+
331+
const escaped = value.replace(/"/g, '""');
332+
return /[",\r\n\u0001-\u001f\u007f]/.test(value)
333+
? `"${escaped}"`
334+
: escaped;
335+
}
336+
337+
async function bulkInsert(data: Array<{ email: string; name: string }>) {
327338
const client = await pool.connect();
328339

329340
try {
330341
const stream = client.query(
331342
copyFrom(`COPY users (email, name) FROM STDIN WITH (FORMAT csv)`)
332343
);
333344

334-
const input = Readable.from(
335-
data.map(row => `${row.email},${row.name}\n`)
336-
);
345+
const input = Readable.from(data.map((row) => [
346+
encodeCsvField(row.email),
347+
encodeCsvField(row.name),
348+
].join(',') + '\n'));
337349

338350
await pipeline(input, stream);
339351
} finally {
@@ -376,10 +388,10 @@ export async function handler() {
376388
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
377389
const db = drizzle(pool);
378390

379-
const users = await db.select().from(users);
391+
const requestUsers = await db.select().from(users);
380392

381393
await pool.end();
382-
return users;
394+
return requestUsers;
383395
}
384396

385397
// ✅ Good: Reuse connection across warm starts
@@ -394,8 +406,8 @@ export async function handler() {
394406
cachedDb = drizzle(pool);
395407
}
396408

397-
const users = await cachedDb.select().from(users);
398-
return users;
409+
const warmUsers = await cachedDb.select().from(users);
410+
return warmUsers;
399411
}
400412
```
401413

@@ -410,7 +422,7 @@ const sql = neon(process.env.DATABASE_URL!);
410422
const db = drizzle(sql);
411423

412424
// Each query is a single HTTP request
413-
const users = await db.select().from(users);
425+
const httpUsers = await db.select().from(users);
414426
```
415427

416428
## Read Replicas
@@ -447,8 +459,16 @@ import { drizzle } from 'drizzle-orm/node-postgres';
447459
const db = drizzle(pool, {
448460
logger: {
449461
logQuery(query: string, params: unknown[]) {
462+
const safeParams = params.map((param) => {
463+
if (param === null || typeof param === 'number' || typeof param === 'boolean') {
464+
return param;
465+
}
466+
467+
return '[REDACTED]';
468+
});
469+
450470
console.log('Query:', query);
451-
console.log('Params:', params);
471+
console.log('Params:', safeParams);
452472
console.time('query');
453473
},
454474
},
@@ -512,7 +532,7 @@ async function measureQuery<T>(
512532
}
513533

514534
// Usage
515-
const users = await measureQuery(
535+
const measuredUsers = await measureQuery(
516536
'fetchUsers',
517537
db.select().from(users).limit(100)
518538
);
@@ -571,13 +591,16 @@ sqlite.pragma('mmap_size = 30000000000'); // 30GB mmap
571591
// Disable for bulk inserts
572592
const stmt = sqlite.prepare('INSERT INTO users (email, name) VALUES (?, ?)');
573593

574-
const insertMany = sqlite.transaction((users) => {
575-
for (const user of users) {
594+
const insertMany = sqlite.transaction((userRows: Array<{ email: string; name: string }>) => {
595+
for (const user of userRows) {
576596
stmt.run(user.email, user.name);
577597
}
578598
});
579599

580-
insertMany(users); // 100x faster than individual inserts
600+
const userRows = [
601+
{ email: 'user@example.com', name: 'User' },
602+
];
603+
insertMany(userRows); // 100x faster than individual inserts
581604
```
582605

583606
## Best Practices Summary

0 commit comments

Comments
 (0)