codecruise 0.1.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/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/codecruise.js +68 -0
- package/config/CLAUDE.md +107 -0
- package/config/agents/analyst.md +48 -0
- package/config/agents/architect-reviewer.md +161 -0
- package/config/agents/architect.md +119 -0
- package/config/agents/critic.md +63 -0
- package/config/agents/developer.md +96 -0
- package/config/agents/devops.md +81 -0
- package/config/agents/orchestrator.md +91 -0
- package/config/agents/planner.md +139 -0
- package/config/agents/retro.md +52 -0
- package/config/agents/reviewer.md +101 -0
- package/config/agents/security-reviewer.md +57 -0
- package/config/agents/stack/expo/AGENT.md +473 -0
- package/config/agents/stack/expo/rules/critical.md +427 -0
- package/config/agents/stack/expo/rules/native.md +455 -0
- package/config/agents/stack/expo/rules/navigation.md +445 -0
- package/config/agents/stack/expo/rules/performance.md +415 -0
- package/config/agents/stack/fastify/AGENT.md +397 -0
- package/config/agents/stack/fastify/rules/api-design.md +283 -0
- package/config/agents/stack/fastify/rules/critical.md +232 -0
- package/config/agents/stack/fastify/rules/queues.md +303 -0
- package/config/agents/stack/fastify/rules/security.md +384 -0
- package/config/agents/stack/index.yaml +48 -0
- package/config/agents/stack/nextjs/AGENT.md +421 -0
- package/config/agents/stack/nextjs/rules/components.md +413 -0
- package/config/agents/stack/nextjs/rules/critical.md +391 -0
- package/config/agents/stack/nextjs/rules/performance.md +403 -0
- package/config/agents/stack/nextjs/rules/styling.md +334 -0
- package/config/agents/stack/shared-ts/AGENT.md +384 -0
- package/config/agents/stack/shared-ts/rules/critical.md +315 -0
- package/config/agents/stack/shared-ts/rules/patterns.md +384 -0
- package/config/agents/stack/shared-ts/rules/zod.md +427 -0
- package/config/agents/tester.md +79 -0
- package/config/commands/architect-discuss.md +366 -0
- package/config/commands/architect-list.md +160 -0
- package/config/commands/architect-review.md +111 -0
- package/config/commands/architect.md +118 -0
- package/config/commands/compact.md +118 -0
- package/config/commands/companion.md +279 -0
- package/config/commands/dashboard.md +152 -0
- package/config/commands/doctor.md +227 -0
- package/config/commands/dogfood-report.md +101 -0
- package/config/commands/flags/run-autonomous.md +110 -0
- package/config/commands/flags/run-pause.md +80 -0
- package/config/commands/ingest.md +173 -0
- package/config/commands/init.md +128 -0
- package/config/commands/metrics.md +87 -0
- package/config/commands/parallel.md +320 -0
- package/config/commands/pause.md +55 -0
- package/config/commands/plan-review.md +130 -0
- package/config/commands/plan.md +216 -0
- package/config/commands/production-check.md +308 -0
- package/config/commands/refine.md +323 -0
- package/config/commands/resume.md +72 -0
- package/config/commands/retro.md +121 -0
- package/config/commands/retry.md +75 -0
- package/config/commands/role.md +310 -0
- package/config/commands/run.md +417 -0
- package/config/commands/scope.md +85 -0
- package/config/commands/setup-permissions.md +104 -0
- package/config/commands/skip.md +75 -0
- package/config/commands/spec-forge.md +213 -0
- package/config/commands/spec-help.md +194 -0
- package/config/commands/spec-patch.md +342 -0
- package/config/commands/spec-resolve.md +110 -0
- package/config/commands/spec-review.md +153 -0
- package/config/commands/status.md +114 -0
- package/config/commands/sync.md +131 -0
- package/config/commands/task.md +138 -0
- package/config/commands/verify.md +124 -0
- package/config/hooks/README.md +632 -0
- package/config/hooks/activity-log.sh +187 -0
- package/config/hooks/anti-rationalize.sh +52 -0
- package/config/hooks/capture-verification.sh +112 -0
- package/config/hooks/collect-metrics.sh +135 -0
- package/config/hooks/enforce-file-scope.sh +75 -0
- package/config/hooks/enforce-state-machine.sh +161 -0
- package/config/hooks/enforce-tdd.sh +180 -0
- package/config/hooks/format.sh +40 -0
- package/config/hooks/lib/activity-helpers.sh +162 -0
- package/config/hooks/lib/read-settings.sh +71 -0
- package/config/hooks/load-context-skills.sh +95 -0
- package/config/hooks/notify.sh +81 -0
- package/config/hooks/pre-commit.sample +35 -0
- package/config/hooks/protect-files.sh +63 -0
- package/config/hooks/track-agents.sh +41 -0
- package/config/hooks/track-commands.sh +37 -0
- package/config/hooks/track-enforcement.sh +44 -0
- package/config/hooks/track-ooda.sh +77 -0
- package/config/hooks/validate-commit-msg.sh +35 -0
- package/config/hooks/validate-plan.sh +213 -0
- package/config/hooks/verify-criteria.sh +46 -0
- package/config/hooks/verify-todo-completion.sh +140 -0
- package/config/rules/comments.md +25 -0
- package/config/rules/decision-rules.md +308 -0
- package/config/rules/hygiene.md +247 -0
- package/config/rules/pattern-detection.md +372 -0
- package/config/rules/profiles.md +193 -0
- package/config/rules/recovery.md +83 -0
- package/config/rules/scope-detection.md +213 -0
- package/config/rules/standards.md +127 -0
- package/config/rules/workflow.md +121 -0
- package/config/schemas.md +767 -0
- package/config/settings.json +195 -0
- package/config/skills/backend/SKILL.md +734 -0
- package/config/skills/database/SKILL.md +426 -0
- package/config/skills/frontend/SKILL.md +434 -0
- package/config/skills/git/SKILL.md +396 -0
- package/config/skills/index.yaml +36 -0
- package/config/skills/observability/SKILL.md +430 -0
- package/config/skills/package-dev/SKILL.md +498 -0
- package/config/skills/performance/SKILL.md +378 -0
- package/config/skills/resilience/SKILL.md +573 -0
- package/config/skills/testing/SKILL.md +398 -0
- package/config/skills/testing-patterns/SKILL.md +276 -0
- package/config/skills/typescript/SKILL.md +152 -0
- package/config/templates/CLAUDE.md +70 -0
- package/config/templates/README.md +117 -0
- package/config/templates/steering/adr-template.md +102 -0
- package/config/templates/steering/product.md +60 -0
- package/config/templates/steering/rfc-template.md +159 -0
- package/config/templates/steering/structure.md +146 -0
- package/config/templates/steering/tech.md +85 -0
- package/package.json +40 -0
- package/src/install.js +163 -0
- package/src/report.js +310 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: db-patterns
|
|
3
|
+
description: Database patterns - N+1 prevention, transactions, cursors, migrations
|
|
4
|
+
keywords: [database, query, transaction, migration, drizzle, prisma, n+1, cursor, pagination]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Database Standards
|
|
8
|
+
|
|
9
|
+
Query performance, transactions, and migration best practices.
|
|
10
|
+
|
|
11
|
+
## Query Performance
|
|
12
|
+
|
|
13
|
+
### N+1 Prevention
|
|
14
|
+
|
|
15
|
+
**N+1 queries are blockers in code review.**
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// ❌ N+1 PROBLEM
|
|
19
|
+
const users = await db.user.findMany();
|
|
20
|
+
for (const user of users) {
|
|
21
|
+
const orders = await db.order.findMany({ where: { userId: user.id } });
|
|
22
|
+
// This executes N+1 queries!
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ✅ EAGER LOADING (Prisma)
|
|
26
|
+
const users = await db.user.findMany({
|
|
27
|
+
include: {
|
|
28
|
+
orders: true,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ✅ BATCH LOADING (DataLoader pattern)
|
|
33
|
+
const ordersByUserLoader = new DataLoader(async (userIds: string[]) => {
|
|
34
|
+
const orders = await db.order.findMany({
|
|
35
|
+
where: { userId: { in: userIds } },
|
|
36
|
+
});
|
|
37
|
+
return userIds.map(id => orders.filter(o => o.userId === id));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Now fetches in batches
|
|
41
|
+
const users = await db.user.findMany();
|
|
42
|
+
const ordersPerUser = await Promise.all(
|
|
43
|
+
users.map(u => ordersByUserLoader.load(u.id))
|
|
44
|
+
);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Index Requirements
|
|
48
|
+
|
|
49
|
+
| Column Type | Index Required |
|
|
50
|
+
|-------------|----------------|
|
|
51
|
+
| Foreign keys | Always |
|
|
52
|
+
| Frequently filtered | Always |
|
|
53
|
+
| Frequently sorted | Always |
|
|
54
|
+
| Unique constraints | Automatic |
|
|
55
|
+
| Full-text search | Specialized (GIN/GIST) |
|
|
56
|
+
|
|
57
|
+
```sql
|
|
58
|
+
-- Foreign keys
|
|
59
|
+
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
|
60
|
+
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
|
|
61
|
+
|
|
62
|
+
-- Composite for common queries
|
|
63
|
+
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
|
|
64
|
+
CREATE INDEX idx_orders_created_at ON orders(created_at DESC);
|
|
65
|
+
|
|
66
|
+
-- Partial index for common filter
|
|
67
|
+
CREATE INDEX idx_orders_pending ON orders(created_at)
|
|
68
|
+
WHERE status = 'pending';
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Query Analysis
|
|
72
|
+
|
|
73
|
+
Before deploying any complex query, run EXPLAIN ANALYZE:
|
|
74
|
+
|
|
75
|
+
```sql
|
|
76
|
+
EXPLAIN ANALYZE
|
|
77
|
+
SELECT o.*, u.email
|
|
78
|
+
FROM orders o
|
|
79
|
+
JOIN users u ON o.user_id = u.id
|
|
80
|
+
WHERE o.status = 'pending'
|
|
81
|
+
AND o.created_at > NOW() - INTERVAL '7 days'
|
|
82
|
+
ORDER BY o.created_at DESC
|
|
83
|
+
LIMIT 100;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Red flags in execution plan:**
|
|
87
|
+
- Sequential scan on large tables
|
|
88
|
+
- Nested loops with large row counts
|
|
89
|
+
- Sort operations without index
|
|
90
|
+
- Hash joins with memory spills
|
|
91
|
+
|
|
92
|
+
### Pagination
|
|
93
|
+
|
|
94
|
+
Never use OFFSET for large datasets:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// ❌ OFFSET pagination (slow on large datasets)
|
|
98
|
+
const page2 = await db.order.findMany({
|
|
99
|
+
skip: 1000,
|
|
100
|
+
take: 100,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ✅ CURSOR pagination (constant time)
|
|
104
|
+
const page2 = await db.order.findMany({
|
|
105
|
+
take: 100,
|
|
106
|
+
cursor: { id: lastSeenId },
|
|
107
|
+
skip: 1, // Skip the cursor itself
|
|
108
|
+
orderBy: { createdAt: 'desc' },
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Transactions
|
|
113
|
+
|
|
114
|
+
### When to Use
|
|
115
|
+
|
|
116
|
+
- Multi-table mutations that must be atomic
|
|
117
|
+
- Read-then-write patterns (check-then-act)
|
|
118
|
+
- Operations requiring consistent snapshots
|
|
119
|
+
- Financial/billing operations (always)
|
|
120
|
+
|
|
121
|
+
### Implementation
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// Prisma interactive transaction
|
|
125
|
+
async function transferFunds(
|
|
126
|
+
fromAccountId: string,
|
|
127
|
+
toAccountId: string,
|
|
128
|
+
amount: number
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
await db.$transaction(async (tx) => {
|
|
131
|
+
// Debit source account
|
|
132
|
+
const fromAccount = await tx.account.update({
|
|
133
|
+
where: { id: fromAccountId },
|
|
134
|
+
data: { balance: { decrement: amount } },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (fromAccount.balance < 0) {
|
|
138
|
+
throw new ValidationError('INSUFFICIENT_FUNDS', 'Insufficient balance');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Credit destination account
|
|
142
|
+
await tx.account.update({
|
|
143
|
+
where: { id: toAccountId },
|
|
144
|
+
data: { balance: { increment: amount } },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Record transfer
|
|
148
|
+
await tx.transfer.create({
|
|
149
|
+
data: {
|
|
150
|
+
fromAccountId,
|
|
151
|
+
toAccountId,
|
|
152
|
+
amount,
|
|
153
|
+
status: 'completed',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Isolation Levels
|
|
161
|
+
|
|
162
|
+
| Level | Use Case | Trade-off |
|
|
163
|
+
|-------|----------|-----------|
|
|
164
|
+
| Read Committed | Default, most operations | Allows non-repeatable reads |
|
|
165
|
+
| Repeatable Read | Financial reports, analytics | Slightly slower |
|
|
166
|
+
| Serializable | Critical financial ops | Slowest, potential retries |
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Set isolation level for sensitive operations
|
|
170
|
+
await db.$transaction(
|
|
171
|
+
async (tx) => {
|
|
172
|
+
// ... sensitive operations
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
isolationLevel: 'Serializable',
|
|
176
|
+
maxWait: 5000,
|
|
177
|
+
timeout: 10000,
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Deadlock Handling
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
async function withRetry<T>(
|
|
186
|
+
operation: () => Promise<T>,
|
|
187
|
+
maxRetries = 3
|
|
188
|
+
): Promise<T> {
|
|
189
|
+
let lastError: Error;
|
|
190
|
+
|
|
191
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
192
|
+
try {
|
|
193
|
+
return await operation();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
lastError = error as Error;
|
|
196
|
+
|
|
197
|
+
// Check if deadlock
|
|
198
|
+
const isDeadlock =
|
|
199
|
+
error instanceof Prisma.PrismaClientKnownRequestError &&
|
|
200
|
+
error.code === 'P2034';
|
|
201
|
+
|
|
202
|
+
if (!isDeadlock || attempt === maxRetries) {
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Exponential backoff
|
|
207
|
+
await sleep(Math.pow(2, attempt) * 100 + Math.random() * 100);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw lastError!;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Usage
|
|
215
|
+
await withRetry(() => transferFunds(from, to, amount));
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Connection Management
|
|
219
|
+
|
|
220
|
+
### Pool Configuration
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Prisma connection pool (via DATABASE_URL)
|
|
224
|
+
// postgresql://user:pass@host:5432/db?connection_limit=20&pool_timeout=10
|
|
225
|
+
|
|
226
|
+
const poolConfig = {
|
|
227
|
+
connectionLimit: Math.floor(maxConnections / numberOfInstances),
|
|
228
|
+
poolTimeout: 10, // seconds to wait for connection
|
|
229
|
+
idleTimeout: 60, // seconds before idle connection is closed
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Connection Limits
|
|
234
|
+
|
|
235
|
+
| Environment | Pool Size | Reasoning |
|
|
236
|
+
|-------------|-----------|-----------|
|
|
237
|
+
| Development | 5 | Single instance |
|
|
238
|
+
| Staging | 10 | Limited replicas |
|
|
239
|
+
| Production | 20-50 | Depends on RDS/instance |
|
|
240
|
+
|
|
241
|
+
Formula: `pool_size = (max_connections - reserved) / num_instances`
|
|
242
|
+
|
|
243
|
+
## Migrations
|
|
244
|
+
|
|
245
|
+
### Requirements
|
|
246
|
+
|
|
247
|
+
1. **Idempotent**: Can run multiple times safely
|
|
248
|
+
2. **Reversible**: Down migration provided
|
|
249
|
+
3. **Tested**: Verified with production-scale data
|
|
250
|
+
4. **Documented**: Breaking changes noted
|
|
251
|
+
|
|
252
|
+
### Migration Template
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// migrations/20240115_add_user_preferences.ts
|
|
256
|
+
|
|
257
|
+
export async function up(db: Knex): Promise<void> {
|
|
258
|
+
// Check if already exists (idempotent)
|
|
259
|
+
const hasTable = await db.schema.hasTable('user_preferences');
|
|
260
|
+
if (hasTable) return;
|
|
261
|
+
|
|
262
|
+
await db.schema.createTable('user_preferences', (table) => {
|
|
263
|
+
table.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'));
|
|
264
|
+
table.uuid('user_id').notNullable().references('users.id').onDelete('CASCADE');
|
|
265
|
+
table.string('theme').defaultTo('light');
|
|
266
|
+
table.boolean('email_notifications').defaultTo(true);
|
|
267
|
+
table.jsonb('settings').defaultTo('{}');
|
|
268
|
+
table.timestamps(true, true);
|
|
269
|
+
|
|
270
|
+
table.unique(['user_id']);
|
|
271
|
+
table.index(['user_id']);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function down(db: Knex): Promise<void> {
|
|
276
|
+
await db.schema.dropTableIfExists('user_preferences');
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Breaking Changes
|
|
281
|
+
|
|
282
|
+
For breaking changes, use multi-phase deployment:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
Phase 1: Add new column (nullable)
|
|
286
|
+
- Deploy code that writes to both old and new
|
|
287
|
+
|
|
288
|
+
Phase 2: Backfill data
|
|
289
|
+
- Migrate existing data to new column
|
|
290
|
+
|
|
291
|
+
Phase 3: Switch reads
|
|
292
|
+
- Deploy code that reads from new column
|
|
293
|
+
|
|
294
|
+
Phase 4: Remove old column
|
|
295
|
+
- Drop old column after verification
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Data Migration Example
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// Large table migration with batching
|
|
302
|
+
export async function up(db: Knex): Promise<void> {
|
|
303
|
+
const BATCH_SIZE = 1000;
|
|
304
|
+
let processed = 0;
|
|
305
|
+
let hasMore = true;
|
|
306
|
+
|
|
307
|
+
while (hasMore) {
|
|
308
|
+
const result = await db.raw(`
|
|
309
|
+
UPDATE users
|
|
310
|
+
SET email_verified = true
|
|
311
|
+
WHERE id IN (
|
|
312
|
+
SELECT id FROM users
|
|
313
|
+
WHERE email_verified IS NULL
|
|
314
|
+
LIMIT ${BATCH_SIZE}
|
|
315
|
+
)
|
|
316
|
+
RETURNING id
|
|
317
|
+
`);
|
|
318
|
+
|
|
319
|
+
processed += result.rowCount;
|
|
320
|
+
hasMore = result.rowCount === BATCH_SIZE;
|
|
321
|
+
|
|
322
|
+
// Allow other operations between batches
|
|
323
|
+
await sleep(100);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.log(`Migrated ${processed} users`);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Soft Deletes
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// Model with soft delete
|
|
334
|
+
model User {
|
|
335
|
+
id String @id @default(uuid())
|
|
336
|
+
email String @unique
|
|
337
|
+
deletedAt DateTime? // Soft delete marker
|
|
338
|
+
|
|
339
|
+
@@index([deletedAt])
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Middleware to filter soft-deleted records
|
|
343
|
+
prisma.$use(async (params, next) => {
|
|
344
|
+
if (params.model === 'User') {
|
|
345
|
+
if (params.action === 'findMany' || params.action === 'findFirst') {
|
|
346
|
+
params.args.where = {
|
|
347
|
+
...params.args.where,
|
|
348
|
+
deletedAt: null,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return next(params);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Soft delete operation
|
|
356
|
+
async function deleteUser(id: string): Promise<void> {
|
|
357
|
+
await db.user.update({
|
|
358
|
+
where: { id },
|
|
359
|
+
data: { deletedAt: new Date() },
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Hard delete (when needed)
|
|
364
|
+
async function permanentlyDeleteUser(id: string): Promise<void> {
|
|
365
|
+
await db.user.delete({ where: { id } });
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Audit Columns
|
|
370
|
+
|
|
371
|
+
All tables should have:
|
|
372
|
+
|
|
373
|
+
```sql
|
|
374
|
+
CREATE TABLE orders (
|
|
375
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
376
|
+
-- ... business columns
|
|
377
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
378
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
379
|
+
created_by UUID REFERENCES users(id),
|
|
380
|
+
updated_by UUID REFERENCES users(id)
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
-- Auto-update updated_at
|
|
384
|
+
CREATE OR REPLACE FUNCTION update_updated_at()
|
|
385
|
+
RETURNS TRIGGER AS $$
|
|
386
|
+
BEGIN
|
|
387
|
+
NEW.updated_at = NOW();
|
|
388
|
+
RETURN NEW;
|
|
389
|
+
END;
|
|
390
|
+
$$ LANGUAGE plpgsql;
|
|
391
|
+
|
|
392
|
+
CREATE TRIGGER orders_updated_at
|
|
393
|
+
BEFORE UPDATE ON orders
|
|
394
|
+
FOR EACH ROW
|
|
395
|
+
EXECUTE FUNCTION update_updated_at();
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Query Timeouts
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// Set statement timeout
|
|
402
|
+
await db.$executeRaw`SET statement_timeout = '30s'`;
|
|
403
|
+
|
|
404
|
+
// Or per-query with Prisma
|
|
405
|
+
const results = await db.$queryRaw`
|
|
406
|
+
SELECT /*+ MAX_EXECUTION_TIME(30000) */ *
|
|
407
|
+
FROM large_table
|
|
408
|
+
WHERE ...
|
|
409
|
+
`;
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Quality Checklist
|
|
413
|
+
|
|
414
|
+
- [ ] No N+1 queries
|
|
415
|
+
- [ ] Indexes on all FKs and common filters
|
|
416
|
+
- [ ] Complex queries analyzed with EXPLAIN
|
|
417
|
+
- [ ] Cursor pagination for large datasets
|
|
418
|
+
- [ ] Transactions for multi-step mutations
|
|
419
|
+
- [ ] Deadlock handling implemented
|
|
420
|
+
- [ ] Connection pool sized correctly
|
|
421
|
+
- [ ] Migrations are idempotent
|
|
422
|
+
- [ ] Down migrations provided
|
|
423
|
+
- [ ] Breaking changes use phased rollout
|
|
424
|
+
- [ ] Soft deletes where appropriate
|
|
425
|
+
- [ ] Audit columns on all tables
|
|
426
|
+
- [ ] Query timeouts configured
|