@oalacea/demon 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/CHANGELOG.md +38 -0
- package/LICENSE +23 -0
- package/README.md +103 -0
- package/agents/deps-analyzer.js +366 -0
- package/agents/detector.js +570 -0
- package/agents/fix-engine.js +305 -0
- package/agents/perf-analyzer.js +294 -0
- package/agents/test-generator.js +387 -0
- package/agents/test-runner.js +318 -0
- package/bin/Dockerfile +65 -0
- package/bin/cli.js +455 -0
- package/lib/config.js +237 -0
- package/lib/docker.js +207 -0
- package/lib/reporter.js +297 -0
- package/package.json +34 -0
- package/prompts/DEPS_EFFICIENCY.md +558 -0
- package/prompts/E2E.md +491 -0
- package/prompts/EXECUTE.md +782 -0
- package/prompts/INTEGRATION_API.md +484 -0
- package/prompts/INTEGRATION_DB.md +425 -0
- package/prompts/PERF_API.md +433 -0
- package/prompts/PERF_DB.md +430 -0
- package/prompts/REMEDIATION.md +482 -0
- package/prompts/UNIT.md +260 -0
- package/scripts/dev.js +106 -0
- package/templates/README.md +22 -0
- package/templates/k6/load-test.js +54 -0
- package/templates/playwright/e2e.spec.ts +61 -0
- package/templates/vitest/api.test.ts +51 -0
- package/templates/vitest/component.test.ts +27 -0
- package/templates/vitest/hook.test.ts +36 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# Database Performance Guide
|
|
2
|
+
|
|
3
|
+
This prompt is included by EXECUTE.md. It provides detailed guidance for database query performance testing.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Query Benchmarking (Vitest)
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// tests/db/benchmarks.test.ts
|
|
11
|
+
import { bench, describe, beforeAll } from 'vitest';
|
|
12
|
+
import { prisma } from '@/lib/db';
|
|
13
|
+
|
|
14
|
+
describe('Database Query Performance', () => {
|
|
15
|
+
let userId: string;
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
// Setup test data
|
|
19
|
+
const user = await prisma.user.create({
|
|
20
|
+
data: {
|
|
21
|
+
email: 'bench@example.com',
|
|
22
|
+
name: 'Benchmark User',
|
|
23
|
+
posts: {
|
|
24
|
+
create: Array.from({ length: 100 }, (_, i) => ({
|
|
25
|
+
title: `Post ${i}`,
|
|
26
|
+
content: `Content ${i}`.repeat(10),
|
|
27
|
+
})),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
userId = user.id;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('Index Usage', () => {
|
|
35
|
+
bench('SELECT by indexed field (email)', async () => {
|
|
36
|
+
await prisma.user.findUnique({
|
|
37
|
+
where: { email: 'bench@example.com' },
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
bench('SELECT by indexed field (id)', async () => {
|
|
42
|
+
await prisma.user.findUnique({
|
|
43
|
+
where: { id: userId },
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
bench('SELECT by non-indexed field (name)', async () => {
|
|
48
|
+
await prisma.user.findFirst({
|
|
49
|
+
where: { name: 'Benchmark User' },
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('Loading Strategies', () => {
|
|
55
|
+
bench('N+1 pattern (bad)', async () => {
|
|
56
|
+
const users = await prisma.user.findMany({ take: 10 });
|
|
57
|
+
for (const user of users) {
|
|
58
|
+
await prisma.post.findMany({
|
|
59
|
+
where: { authorId: user.id },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
bench('Eager loading with include (good)', async () => {
|
|
65
|
+
await prisma.user.findMany({
|
|
66
|
+
take: 10,
|
|
67
|
+
include: { posts: true },
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
bench('Eager loading with select (optimal)', async () => {
|
|
72
|
+
await prisma.user.findMany({
|
|
73
|
+
take: 10,
|
|
74
|
+
select: {
|
|
75
|
+
id: true,
|
|
76
|
+
name: true,
|
|
77
|
+
posts: {
|
|
78
|
+
select: {
|
|
79
|
+
id: true,
|
|
80
|
+
title: true,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('Pagination', () => {
|
|
89
|
+
bench('Offset-based pagination', async () => {
|
|
90
|
+
await prisma.user.findMany({
|
|
91
|
+
skip: 50,
|
|
92
|
+
take: 10,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
bench('Cursor-based pagination', async () => {
|
|
97
|
+
await prisma.user.findMany({
|
|
98
|
+
take: 10,
|
|
99
|
+
cursor: { id: userId },
|
|
100
|
+
skip: 1,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Aggregations', () => {
|
|
106
|
+
bench('COUNT query', async () => {
|
|
107
|
+
await prisma.user.count();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
bench('COUNT with index', async () => {
|
|
111
|
+
await prisma.post.count({
|
|
112
|
+
where: { authorId: userId },
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
bench('Complex aggregation', async () => {
|
|
117
|
+
await prisma.post.aggregate({
|
|
118
|
+
where: { authorId: userId },
|
|
119
|
+
_count: true,
|
|
120
|
+
_avg: { likes: true },
|
|
121
|
+
_sum: { views: true },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Transaction Performance', () => {
|
|
127
|
+
bench('Single write', async () => {
|
|
128
|
+
await prisma.user.create({
|
|
129
|
+
data: {
|
|
130
|
+
email: `test-${Date.now()}@example.com`,
|
|
131
|
+
name: 'Test',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
bench('Batch write (10 records)', async () => {
|
|
137
|
+
await prisma.user.createMany({
|
|
138
|
+
data: Array.from({ length: 10 }, (_, i) => ({
|
|
139
|
+
email: `batch-${Date.now()}-${i}@example.com`,
|
|
140
|
+
name: `User ${i}`,
|
|
141
|
+
})),
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
bench('Transaction with multiple writes', async () => {
|
|
146
|
+
await prisma.$transaction([
|
|
147
|
+
prisma.user.create({
|
|
148
|
+
data: {
|
|
149
|
+
email: `trans-${Date.now()}@example.com`,
|
|
150
|
+
name: 'Trans User',
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
prisma.post.create({
|
|
154
|
+
data: {
|
|
155
|
+
title: 'Trans Post',
|
|
156
|
+
content: 'Content',
|
|
157
|
+
author: {
|
|
158
|
+
connect: { email: `trans-${Date.now()}@example.com` },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Query Analysis
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// tests/db/analysis.test.ts
|
|
174
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
175
|
+
import { prisma } from '@/lib/db';
|
|
176
|
+
|
|
177
|
+
describe('Query Analysis', () => {
|
|
178
|
+
describe('N+1 Detection', () => {
|
|
179
|
+
it('should not have N+1 in user posts query', async () => {
|
|
180
|
+
const queries: string[] = [];
|
|
181
|
+
|
|
182
|
+
// Log queries (requires Prisma preview feature)
|
|
183
|
+
const originalExecute = prisma.$queryRawUnsafe.bind(prisma);
|
|
184
|
+
prisma.$queryRawUnsafe = (...args: any[]) => {
|
|
185
|
+
queries.push(args[0]);
|
|
186
|
+
return originalExecute(...args);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Execute the query pattern
|
|
190
|
+
const users = await prisma.user.findMany({
|
|
191
|
+
take: 5,
|
|
192
|
+
include: { posts: true },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Should only be 2 queries (users + posts via join)
|
|
196
|
+
// Not N+1 (1 for users + N for posts)
|
|
197
|
+
expect(queries.length).toBeLessThanOrEqual(2);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('Index Usage', () => {
|
|
202
|
+
it('should use index for email lookup', async () => {
|
|
203
|
+
// This is a conceptual test - actual implementation varies
|
|
204
|
+
// Use EXPLAIN ANALYZE for real verification
|
|
205
|
+
|
|
206
|
+
const start = Date.now();
|
|
207
|
+
await prisma.user.findUnique({
|
|
208
|
+
where: { email: 'test@example.com' },
|
|
209
|
+
});
|
|
210
|
+
const indexedTime = Date.now() - start;
|
|
211
|
+
|
|
212
|
+
// Compare with non-indexed lookup
|
|
213
|
+
const start2 = Date.now();
|
|
214
|
+
await prisma.user.findFirst({
|
|
215
|
+
where: { name: 'Test User' },
|
|
216
|
+
});
|
|
217
|
+
const nonIndexedTime = Date.now() - start2;
|
|
218
|
+
|
|
219
|
+
// Indexed should be faster (with significant data)
|
|
220
|
+
expect(indexedTime).toBeLessThan(nonIndexedTime);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('Connection Pool', () => {
|
|
225
|
+
it('should handle concurrent queries efficiently', async () => {
|
|
226
|
+
const start = Date.now();
|
|
227
|
+
|
|
228
|
+
await Promise.all([
|
|
229
|
+
prisma.user.findMany({ take: 10 }),
|
|
230
|
+
prisma.post.findMany({ take: 10 }),
|
|
231
|
+
prisma.comment.findMany({ take: 10 }),
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const concurrentTime = Date.now() - start;
|
|
235
|
+
|
|
236
|
+
// Sequential time
|
|
237
|
+
const start2 = Date.now();
|
|
238
|
+
await prisma.user.findMany({ take: 10 });
|
|
239
|
+
await prisma.post.findMany({ take: 10 });
|
|
240
|
+
await prisma.comment.findMany({ take: 10 });
|
|
241
|
+
const sequentialTime = Date.now() - start2;
|
|
242
|
+
|
|
243
|
+
// Concurrent should be faster
|
|
244
|
+
expect(concurrentTime).toBeLessThan(sequentialTime);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Prisma-Specific Patterns
|
|
253
|
+
|
|
254
|
+
### Include vs Select
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
describe('Field Selection Performance', () => {
|
|
258
|
+
it('should prefer select over include for large data', async () => {
|
|
259
|
+
// Include loads all fields
|
|
260
|
+
const start1 = Date.now();
|
|
261
|
+
await prisma.user.findMany({
|
|
262
|
+
take: 100,
|
|
263
|
+
include: { posts: true },
|
|
264
|
+
});
|
|
265
|
+
const includeTime = Date.now() - start1;
|
|
266
|
+
|
|
267
|
+
// Select loads only needed fields
|
|
268
|
+
const start2 = Date.now();
|
|
269
|
+
await prisma.user.findMany({
|
|
270
|
+
take: 100,
|
|
271
|
+
select: {
|
|
272
|
+
id: true,
|
|
273
|
+
name: true,
|
|
274
|
+
posts: {
|
|
275
|
+
select: { id: true, title: true },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const selectTime = Date.now() - start2;
|
|
280
|
+
|
|
281
|
+
// Select should be faster for large datasets
|
|
282
|
+
expect(selectTime).toBeLessThan(includeTime);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Lazy vs Eager Loading
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
describe('Loading Strategy', () => {
|
|
291
|
+
bench('Lazy loading (deferred)', async () => {
|
|
292
|
+
const user = await prisma.user.findUnique({
|
|
293
|
+
where: { id: userId },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (user) {
|
|
297
|
+
await prisma.post.findMany({
|
|
298
|
+
where: { authorId: user.id },
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
bench('Eager loading (include)', async () => {
|
|
304
|
+
await prisma.user.findUnique({
|
|
305
|
+
where: { id: userId },
|
|
306
|
+
include: { posts: true },
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Performance Assertions
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// tests/db/performance-assertions.test.ts
|
|
318
|
+
import { describe, it, expect } from 'vitest';
|
|
319
|
+
import { prisma } from '@/lib/db';
|
|
320
|
+
|
|
321
|
+
describe('Performance Assertions', () => {
|
|
322
|
+
it('should return user list within 100ms', async () => {
|
|
323
|
+
const start = Date.now();
|
|
324
|
+
await prisma.user.findMany({ take: 50 });
|
|
325
|
+
const duration = Date.now() - start;
|
|
326
|
+
|
|
327
|
+
expect(duration).toBeLessThan(100);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should create user within 50ms', async () => {
|
|
331
|
+
const start = Date.now();
|
|
332
|
+
await prisma.user.create({
|
|
333
|
+
data: {
|
|
334
|
+
email: `perf-${Date.now()}@example.com`,
|
|
335
|
+
name: 'Perf Test',
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
const duration = Date.now() - start;
|
|
339
|
+
|
|
340
|
+
expect(duration).toBeLessThan(50);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should handle 100 concurrent queries without timeouts', async () => {
|
|
344
|
+
const results = await Promise.allSettled(
|
|
345
|
+
Array.from({ length: 100 }, () =>
|
|
346
|
+
prisma.user.findFirst({ where: { id: userId } })
|
|
347
|
+
)
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const failures = results.filter((r) => r.status === 'rejected');
|
|
351
|
+
expect(failures).toHaveLength(0);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Migration Performance
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// tests/db/migration-performance.test.ts
|
|
362
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
363
|
+
import { prisma } from '@/lib/db';
|
|
364
|
+
|
|
365
|
+
describe('Migration Performance', () => {
|
|
366
|
+
beforeAll(async () => {
|
|
367
|
+
// Ensure clean state
|
|
368
|
+
await prisma.user.deleteMany({});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
afterAll(async () => {
|
|
372
|
+
await prisma.user.deleteMany({});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should migrate 1000 records efficiently', async () => {
|
|
376
|
+
const start = Date.now();
|
|
377
|
+
|
|
378
|
+
// Batch insert
|
|
379
|
+
await prisma.user.createMany({
|
|
380
|
+
data: Array.from({ length: 1000 }, (_, i) => ({
|
|
381
|
+
email: `migrate-${i}@example.com`,
|
|
382
|
+
name: `User ${i}`,
|
|
383
|
+
})),
|
|
384
|
+
skipDuplicates: true,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const duration = Date.now() - start;
|
|
388
|
+
|
|
389
|
+
// Should complete in reasonable time
|
|
390
|
+
expect(duration).toBeLessThan(5000);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should update 1000 records efficiently', async () => {
|
|
394
|
+
// First create
|
|
395
|
+
await prisma.user.createMany({
|
|
396
|
+
data: Array.from({ length: 1000 }, (_, i) => ({
|
|
397
|
+
email: `update-${i}@example.com`,
|
|
398
|
+
name: `User ${i}`,
|
|
399
|
+
})),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const start = Date.now();
|
|
403
|
+
|
|
404
|
+
// Batch update
|
|
405
|
+
await prisma.user.updateMany({
|
|
406
|
+
where: { email: { startsWith: 'update-' } },
|
|
407
|
+
data: { name: 'Updated' },
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const duration = Date.now() - start;
|
|
411
|
+
|
|
412
|
+
expect(duration).toBeLessThan(2000);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Running DB Performance Tests
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
# Run all performance tests
|
|
423
|
+
docker exec demon-tools npm test -- tests/db/benchmarks.test.ts --reporter=verbose
|
|
424
|
+
|
|
425
|
+
# Run specific benchmark
|
|
426
|
+
docker exec demon-tools npm test -- tests/db/benchmarks.test.ts --bench
|
|
427
|
+
|
|
428
|
+
# Run with memory profiling
|
|
429
|
+
docker exec demon-tools node --expose-gc -r ts-node/register tests/db/benchmarks.test.ts
|
|
430
|
+
```
|