bunsane 0.2.3 → 0.2.7
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/config/cache.config.ts +2 -0
- package/core/ArcheType.ts +67 -34
- package/core/BatchLoader.ts +215 -30
- package/core/Entity.ts +2 -2
- package/core/RequestContext.ts +15 -10
- package/core/RequestLoaders.ts +4 -2
- package/core/cache/CacheFactory.ts +3 -1
- package/core/cache/CacheProvider.ts +1 -0
- package/core/cache/CacheWarmer.ts +45 -23
- package/core/cache/MemoryCache.ts +10 -1
- package/core/cache/RedisCache.ts +26 -7
- package/core/validateEnv.ts +8 -0
- package/database/DatabaseHelper.ts +113 -1
- package/database/index.ts +78 -45
- package/docs/SCALABILITY_PLAN.md +175 -0
- package/package.json +13 -2
- package/query/CTENode.ts +44 -24
- package/query/ComponentInclusionNode.ts +181 -91
- package/query/Query.ts +9 -9
- package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +338 -0
- package/tests/benchmark/bunfig.toml +9 -0
- package/tests/benchmark/fixtures/EcommerceComponents.ts +283 -0
- package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +301 -0
- package/tests/benchmark/fixtures/RelationTracker.ts +159 -0
- package/tests/benchmark/fixtures/index.ts +6 -0
- package/tests/benchmark/index.ts +22 -0
- package/tests/benchmark/noop-preload.ts +3 -0
- package/tests/benchmark/runners/BenchmarkLoader.ts +132 -0
- package/tests/benchmark/runners/index.ts +4 -0
- package/tests/benchmark/scenarios/query-benchmarks.test.ts +465 -0
- package/tests/benchmark/scripts/generate-db.ts +344 -0
- package/tests/benchmark/scripts/run-benchmarks.ts +97 -0
- package/tests/integration/query/Query.complexAnalysis.test.ts +557 -0
- package/tests/integration/query/Query.explainAnalyze.test.ts +233 -0
- package/tests/stress/fixtures/RealisticComponents.ts +235 -0
- package/tests/stress/scenarios/realistic-scenarios.test.ts +1081 -0
- package/tests/stress/scenarios/timeout-investigation.test.ts +522 -0
- package/tests/unit/BatchLoader.test.ts +139 -25
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complex Query Performance Analysis Tests
|
|
3
|
+
* Analyzes query plans for performance issues
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect, beforeAll, beforeEach, afterAll } from 'bun:test';
|
|
6
|
+
import { Query, FilterOp } from '../../../query/Query';
|
|
7
|
+
import { Entity } from '../../../core/Entity';
|
|
8
|
+
import { TestUser, TestProduct, TestOrder } from '../../fixtures/components';
|
|
9
|
+
import { ensureComponentsRegistered } from '../../utils';
|
|
10
|
+
import { CacheManager } from '../../../core/cache';
|
|
11
|
+
import EntityManager from '../../../core/EntityManager';
|
|
12
|
+
import db from '../../../database';
|
|
13
|
+
|
|
14
|
+
interface QueryPlanAnalysis {
|
|
15
|
+
plan: string;
|
|
16
|
+
hasSeqScan: boolean;
|
|
17
|
+
hasIndexScan: boolean;
|
|
18
|
+
hasNestedLoop: boolean;
|
|
19
|
+
hasHashJoin: boolean;
|
|
20
|
+
planningTimeMs: number;
|
|
21
|
+
executionTimeMs: number;
|
|
22
|
+
totalRows: number;
|
|
23
|
+
warnings: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function analyzeQueryPlan(plan: string): QueryPlanAnalysis {
|
|
27
|
+
const warnings: string[] = [];
|
|
28
|
+
|
|
29
|
+
const hasSeqScan = /Seq Scan/i.test(plan);
|
|
30
|
+
const hasIndexScan = /Index Scan|Index Only Scan|Bitmap Index Scan/i.test(plan);
|
|
31
|
+
const hasNestedLoop = /Nested Loop/i.test(plan);
|
|
32
|
+
const hasHashJoin = /Hash Join/i.test(plan);
|
|
33
|
+
|
|
34
|
+
// Extract timing
|
|
35
|
+
const planningMatch = plan.match(/Planning Time:\s*([\d.]+)\s*ms/i);
|
|
36
|
+
const executionMatch = plan.match(/Execution Time:\s*([\d.]+)\s*ms/i);
|
|
37
|
+
const planningTimeMs = planningMatch ? parseFloat(planningMatch[1]!) : 0;
|
|
38
|
+
const executionTimeMs = executionMatch ? parseFloat(executionMatch[1]!) : 0;
|
|
39
|
+
|
|
40
|
+
// Extract row counts
|
|
41
|
+
const rowsMatch = plan.match(/rows=(\d+)/g);
|
|
42
|
+
const totalRows = rowsMatch
|
|
43
|
+
? rowsMatch.reduce((sum, m) => sum + parseInt(m.replace('rows=', '')), 0)
|
|
44
|
+
: 0;
|
|
45
|
+
|
|
46
|
+
// Check for potential issues
|
|
47
|
+
if (hasSeqScan && !hasIndexScan) {
|
|
48
|
+
warnings.push('Sequential scan (expected for small tables)');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (hasNestedLoop && totalRows > 1000) {
|
|
52
|
+
warnings.push('Nested loop with many rows');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
plan,
|
|
57
|
+
hasSeqScan,
|
|
58
|
+
hasIndexScan,
|
|
59
|
+
hasNestedLoop,
|
|
60
|
+
hasHashJoin,
|
|
61
|
+
planningTimeMs,
|
|
62
|
+
executionTimeMs,
|
|
63
|
+
totalRows,
|
|
64
|
+
warnings
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('Complex Query Performance Analysis', () => {
|
|
69
|
+
// Configurable via PERF_ENTITY_COUNT env var (default: 50, max recommended: 50000)
|
|
70
|
+
const ENTITY_COUNT = parseInt(process.env.PERF_ENTITY_COUNT || '50', 10);
|
|
71
|
+
const BATCH_SIZE = 100; // Insert in batches for better performance
|
|
72
|
+
const createdEntityIds: string[] = [];
|
|
73
|
+
|
|
74
|
+
beforeAll(async () => {
|
|
75
|
+
await ensureComponentsRegistered(TestUser, TestProduct, TestOrder);
|
|
76
|
+
|
|
77
|
+
// Initialize cache
|
|
78
|
+
(EntityManager as any).dbReady = true;
|
|
79
|
+
const cacheManager = CacheManager.getInstance();
|
|
80
|
+
await cacheManager.initialize({
|
|
81
|
+
enabled: true,
|
|
82
|
+
provider: 'memory',
|
|
83
|
+
strategy: 'write-through',
|
|
84
|
+
defaultTTL: 3600000,
|
|
85
|
+
entity: { enabled: true, ttl: 3600000 },
|
|
86
|
+
component: { enabled: true, ttl: 1800000 },
|
|
87
|
+
query: { enabled: false, ttl: 300000, maxSize: 10000 }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Create test dataset (not using tracker so data persists)
|
|
91
|
+
console.log(`\n${'='.repeat(60)}`);
|
|
92
|
+
console.log(`Creating ${ENTITY_COUNT.toLocaleString()} test entities for performance analysis...`);
|
|
93
|
+
console.log(`(Set PERF_ENTITY_COUNT env var to change: 10000, 50000, etc.)`);
|
|
94
|
+
console.log(`${'='.repeat(60)}\n`);
|
|
95
|
+
|
|
96
|
+
const startTime = performance.now();
|
|
97
|
+
let lastProgressTime = startTime;
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < ENTITY_COUNT; i++) {
|
|
100
|
+
const entity = Entity.Create();
|
|
101
|
+
createdEntityIds.push(entity.id);
|
|
102
|
+
|
|
103
|
+
entity.add(TestUser, {
|
|
104
|
+
name: `PerfUser${i}`,
|
|
105
|
+
email: `perf${i}@example.com`,
|
|
106
|
+
age: 20 + (i % 50)
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (i % 2 === 0) {
|
|
110
|
+
entity.add(TestProduct, {
|
|
111
|
+
sku: `PERF-SKU-${i}`,
|
|
112
|
+
name: `Performance Product ${i}`,
|
|
113
|
+
price: 10 + (i % 1000) * 5, // Vary prices
|
|
114
|
+
inStock: i % 3 !== 0
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (i % 3 === 0) {
|
|
119
|
+
entity.add(TestOrder, {
|
|
120
|
+
orderId: `ORD-${i}`,
|
|
121
|
+
total: 100 + (i % 500) * 10,
|
|
122
|
+
status: i % 2 === 0 ? 'completed' : 'pending'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await entity.save();
|
|
127
|
+
|
|
128
|
+
// Progress indicator for large datasets
|
|
129
|
+
if (ENTITY_COUNT >= 1000 && (i + 1) % 1000 === 0) {
|
|
130
|
+
const now = performance.now();
|
|
131
|
+
const elapsed = (now - startTime) / 1000;
|
|
132
|
+
const rate = (i + 1) / elapsed;
|
|
133
|
+
const remaining = (ENTITY_COUNT - i - 1) / rate;
|
|
134
|
+
console.log(` Progress: ${i + 1}/${ENTITY_COUNT} (${((i + 1) / ENTITY_COUNT * 100).toFixed(1)}%) - ${rate.toFixed(0)} entities/sec - ETA: ${remaining.toFixed(1)}s`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const totalTime = (performance.now() - startTime) / 1000;
|
|
139
|
+
console.log(`\nTest data created in ${totalTime.toFixed(2)}s (${(ENTITY_COUNT / totalTime).toFixed(0)} entities/sec)\n`);
|
|
140
|
+
|
|
141
|
+
// Run ANALYZE to update PostgreSQL statistics for better query planning
|
|
142
|
+
if (ENTITY_COUNT >= 1000) {
|
|
143
|
+
console.log('Running ANALYZE to update statistics...');
|
|
144
|
+
await db.unsafe(`ANALYZE entities`);
|
|
145
|
+
await db.unsafe(`ANALYZE components`);
|
|
146
|
+
await db.unsafe(`ANALYZE entity_components`);
|
|
147
|
+
console.log('Statistics updated.\n');
|
|
148
|
+
}
|
|
149
|
+
}, 600000); // 10 minute timeout for large datasets
|
|
150
|
+
|
|
151
|
+
afterAll(async () => {
|
|
152
|
+
// Bulk cleanup for performance
|
|
153
|
+
console.log(`\nCleaning up ${createdEntityIds.length.toLocaleString()} test entities...`);
|
|
154
|
+
const startTime = performance.now();
|
|
155
|
+
|
|
156
|
+
// Delete in batches
|
|
157
|
+
for (let i = 0; i < createdEntityIds.length; i += BATCH_SIZE) {
|
|
158
|
+
const batch = createdEntityIds.slice(i, i + BATCH_SIZE);
|
|
159
|
+
const placeholders = batch.map((_, idx) => `$${idx + 1}`).join(',');
|
|
160
|
+
try {
|
|
161
|
+
await db.unsafe(`DELETE FROM components WHERE entity_id IN (${placeholders})`, batch);
|
|
162
|
+
await db.unsafe(`DELETE FROM entities WHERE id IN (${placeholders})`, batch);
|
|
163
|
+
} catch { }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const duration = (performance.now() - startTime) / 1000;
|
|
167
|
+
console.log(`Cleanup completed in ${duration.toFixed(2)}s\n`);
|
|
168
|
+
}, 600000);
|
|
169
|
+
|
|
170
|
+
describe('Single Component Queries', () => {
|
|
171
|
+
test('simple query without filters', async () => {
|
|
172
|
+
const plan = await new Query()
|
|
173
|
+
.with(TestUser)
|
|
174
|
+
.explainAnalyze();
|
|
175
|
+
|
|
176
|
+
const analysis = analyzeQueryPlan(plan);
|
|
177
|
+
|
|
178
|
+
console.log('\n=== Simple Query (no filters) ===');
|
|
179
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
180
|
+
console.log(`Seq Scan: ${analysis.hasSeqScan}, Index Scan: ${analysis.hasIndexScan}`);
|
|
181
|
+
if (analysis.warnings.length > 0) {
|
|
182
|
+
console.log('Warnings:', analysis.warnings);
|
|
183
|
+
}
|
|
184
|
+
console.log('---\n' + plan.substring(0, 500) + '...\n');
|
|
185
|
+
|
|
186
|
+
expect(analysis.executionTimeMs).toBeLessThan(1000); // Should be fast
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('query with equality filter', async () => {
|
|
190
|
+
const plan = await new Query()
|
|
191
|
+
.with(TestUser, {
|
|
192
|
+
filters: [Query.filter('name', FilterOp.EQ, 'PerfUser25')]
|
|
193
|
+
})
|
|
194
|
+
.explainAnalyze();
|
|
195
|
+
|
|
196
|
+
const analysis = analyzeQueryPlan(plan);
|
|
197
|
+
|
|
198
|
+
console.log('\n=== Equality Filter Query ===');
|
|
199
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
200
|
+
console.log(`Seq Scan: ${analysis.hasSeqScan}, Index Scan: ${analysis.hasIndexScan}`);
|
|
201
|
+
if (analysis.warnings.length > 0) {
|
|
202
|
+
console.log('Warnings:', analysis.warnings);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
expect(analysis.executionTimeMs).toBeLessThan(500);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('query with range filter', async () => {
|
|
209
|
+
const plan = await new Query()
|
|
210
|
+
.with(TestUser, {
|
|
211
|
+
filters: [
|
|
212
|
+
Query.filter('age', FilterOp.GTE, 30),
|
|
213
|
+
Query.filter('age', FilterOp.LTE, 40)
|
|
214
|
+
]
|
|
215
|
+
})
|
|
216
|
+
.explainAnalyze();
|
|
217
|
+
|
|
218
|
+
const analysis = analyzeQueryPlan(plan);
|
|
219
|
+
|
|
220
|
+
console.log('\n=== Range Filter Query ===');
|
|
221
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
222
|
+
console.log(`Seq Scan: ${analysis.hasSeqScan}, Index Scan: ${analysis.hasIndexScan}`);
|
|
223
|
+
|
|
224
|
+
expect(analysis.executionTimeMs).toBeLessThan(500);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('query with LIKE filter', async () => {
|
|
228
|
+
const plan = await new Query()
|
|
229
|
+
.with(TestUser, {
|
|
230
|
+
filters: [Query.filter('email', FilterOp.LIKE, 'perf%@example.com')]
|
|
231
|
+
})
|
|
232
|
+
.explainAnalyze();
|
|
233
|
+
|
|
234
|
+
const analysis = analyzeQueryPlan(plan);
|
|
235
|
+
|
|
236
|
+
console.log('\n=== LIKE Filter Query ===');
|
|
237
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
238
|
+
// LIKE queries often use seq scan which is expected
|
|
239
|
+
|
|
240
|
+
expect(analysis.executionTimeMs).toBeLessThan(500);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('Multi-Component Queries', () => {
|
|
245
|
+
test('two component intersection', async () => {
|
|
246
|
+
const plan = await new Query()
|
|
247
|
+
.with(TestUser)
|
|
248
|
+
.with(TestProduct)
|
|
249
|
+
.explainAnalyze();
|
|
250
|
+
|
|
251
|
+
const analysis = analyzeQueryPlan(plan);
|
|
252
|
+
|
|
253
|
+
console.log('\n=== Two Component Query ===');
|
|
254
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
255
|
+
console.log(`Join type - Nested Loop: ${analysis.hasNestedLoop}, Hash Join: ${analysis.hasHashJoin}`);
|
|
256
|
+
if (analysis.warnings.length > 0) {
|
|
257
|
+
console.log('Warnings:', analysis.warnings);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
expect(analysis.executionTimeMs).toBeLessThan(1000);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('three component intersection', async () => {
|
|
264
|
+
const plan = await new Query()
|
|
265
|
+
.with(TestUser)
|
|
266
|
+
.with(TestProduct)
|
|
267
|
+
.with(TestOrder)
|
|
268
|
+
.explainAnalyze();
|
|
269
|
+
|
|
270
|
+
const analysis = analyzeQueryPlan(plan);
|
|
271
|
+
|
|
272
|
+
console.log('\n=== Three Component Query ===');
|
|
273
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
274
|
+
console.log(`Join type - Nested Loop: ${analysis.hasNestedLoop}, Hash Join: ${analysis.hasHashJoin}`);
|
|
275
|
+
if (analysis.warnings.length > 0) {
|
|
276
|
+
console.log('Warnings:', analysis.warnings);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
expect(analysis.executionTimeMs).toBeLessThan(1000);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('multi-component with filters on each', async () => {
|
|
283
|
+
const plan = await new Query()
|
|
284
|
+
.with(TestUser, {
|
|
285
|
+
filters: [Query.filter('age', FilterOp.GT, 25)]
|
|
286
|
+
})
|
|
287
|
+
.with(TestProduct, {
|
|
288
|
+
filters: [Query.filter('inStock', FilterOp.EQ, true)]
|
|
289
|
+
})
|
|
290
|
+
.explainAnalyze();
|
|
291
|
+
|
|
292
|
+
const analysis = analyzeQueryPlan(plan);
|
|
293
|
+
|
|
294
|
+
console.log('\n=== Multi-Component with Filters ===');
|
|
295
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
296
|
+
if (analysis.warnings.length > 0) {
|
|
297
|
+
console.log('Warnings:', analysis.warnings);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
expect(analysis.executionTimeMs).toBeLessThan(1000);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('Sorting and Pagination', () => {
|
|
305
|
+
test('sorted query', async () => {
|
|
306
|
+
const plan = await new Query()
|
|
307
|
+
.with(TestUser)
|
|
308
|
+
.sortBy(TestUser, 'age', 'DESC')
|
|
309
|
+
.explainAnalyze();
|
|
310
|
+
|
|
311
|
+
const analysis = analyzeQueryPlan(plan);
|
|
312
|
+
|
|
313
|
+
console.log('\n=== Sorted Query ===');
|
|
314
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
315
|
+
console.log(`Has Sort operator: ${/Sort/i.test(plan)}`);
|
|
316
|
+
|
|
317
|
+
expect(analysis.executionTimeMs).toBeLessThan(500);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('paginated query (OFFSET)', async () => {
|
|
321
|
+
const plan = await new Query()
|
|
322
|
+
.with(TestUser)
|
|
323
|
+
.take(10)
|
|
324
|
+
.offset(20)
|
|
325
|
+
.explainAnalyze();
|
|
326
|
+
|
|
327
|
+
const analysis = analyzeQueryPlan(plan);
|
|
328
|
+
|
|
329
|
+
console.log('\n=== Paginated Query (OFFSET) ===');
|
|
330
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
331
|
+
console.log(`Has Limit: ${/Limit/i.test(plan)}`);
|
|
332
|
+
|
|
333
|
+
expect(analysis.executionTimeMs).toBeLessThan(500);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('cursor-based pagination', async () => {
|
|
337
|
+
// First get an entity ID to use as cursor
|
|
338
|
+
const entities = await new Query()
|
|
339
|
+
.with(TestUser)
|
|
340
|
+
.take(15)
|
|
341
|
+
.exec();
|
|
342
|
+
|
|
343
|
+
if (entities.length >= 15) {
|
|
344
|
+
const cursorId = entities[14]!.id;
|
|
345
|
+
|
|
346
|
+
const plan = await new Query()
|
|
347
|
+
.with(TestUser)
|
|
348
|
+
.cursor(cursorId)
|
|
349
|
+
.take(10)
|
|
350
|
+
.explainAnalyze();
|
|
351
|
+
|
|
352
|
+
const analysis = analyzeQueryPlan(plan);
|
|
353
|
+
|
|
354
|
+
console.log('\n=== Cursor-Based Pagination ===');
|
|
355
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
356
|
+
|
|
357
|
+
expect(analysis.executionTimeMs).toBeLessThan(500);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('sorted and paginated', async () => {
|
|
362
|
+
const plan = await new Query()
|
|
363
|
+
.with(TestUser)
|
|
364
|
+
.sortBy(TestUser, 'name', 'ASC')
|
|
365
|
+
.take(10)
|
|
366
|
+
.offset(5)
|
|
367
|
+
.explainAnalyze();
|
|
368
|
+
|
|
369
|
+
const analysis = analyzeQueryPlan(plan);
|
|
370
|
+
|
|
371
|
+
console.log('\n=== Sorted + Paginated Query ===');
|
|
372
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
373
|
+
|
|
374
|
+
expect(analysis.executionTimeMs).toBeLessThan(500);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('Aggregate Queries', () => {
|
|
379
|
+
test('count query', async () => {
|
|
380
|
+
const startTime = performance.now();
|
|
381
|
+
const count = await new Query()
|
|
382
|
+
.with(TestUser, {
|
|
383
|
+
filters: [Query.filter('name', FilterOp.LIKE, 'PerfUser%')]
|
|
384
|
+
})
|
|
385
|
+
.count();
|
|
386
|
+
const duration = performance.now() - startTime;
|
|
387
|
+
|
|
388
|
+
console.log('\n=== Count Query ===');
|
|
389
|
+
console.log(`Count result: ${count}, Duration: ${duration.toFixed(2)}ms`);
|
|
390
|
+
|
|
391
|
+
expect(count).toBeGreaterThan(0);
|
|
392
|
+
expect(duration).toBeLessThan(500);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test('sum query', async () => {
|
|
396
|
+
const startTime = performance.now();
|
|
397
|
+
const sum = await new Query()
|
|
398
|
+
.with(TestProduct, {
|
|
399
|
+
filters: [Query.filter('name', FilterOp.LIKE, 'Performance Product%')]
|
|
400
|
+
})
|
|
401
|
+
.sum(TestProduct, 'price');
|
|
402
|
+
const duration = performance.now() - startTime;
|
|
403
|
+
|
|
404
|
+
console.log('\n=== Sum Query ===');
|
|
405
|
+
console.log(`Sum result: ${sum}, Duration: ${duration.toFixed(2)}ms`);
|
|
406
|
+
|
|
407
|
+
expect(sum).toBeGreaterThan(0);
|
|
408
|
+
expect(duration).toBeLessThan(500);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('average query', async () => {
|
|
412
|
+
const startTime = performance.now();
|
|
413
|
+
const avg = await new Query()
|
|
414
|
+
.with(TestUser, {
|
|
415
|
+
filters: [Query.filter('name', FilterOp.LIKE, 'PerfUser%')]
|
|
416
|
+
})
|
|
417
|
+
.average(TestUser, 'age');
|
|
418
|
+
const duration = performance.now() - startTime;
|
|
419
|
+
|
|
420
|
+
console.log('\n=== Average Query ===');
|
|
421
|
+
console.log(`Average result: ${avg.toFixed(2)}, Duration: ${duration.toFixed(2)}ms`);
|
|
422
|
+
|
|
423
|
+
expect(avg).toBeGreaterThan(0);
|
|
424
|
+
expect(duration).toBeLessThan(500);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('Complex Combined Queries', () => {
|
|
429
|
+
test('full complexity query', async () => {
|
|
430
|
+
const plan = await new Query()
|
|
431
|
+
.with(TestUser, {
|
|
432
|
+
filters: [
|
|
433
|
+
Query.filter('age', FilterOp.GTE, 25),
|
|
434
|
+
Query.filter('age', FilterOp.LTE, 45)
|
|
435
|
+
]
|
|
436
|
+
})
|
|
437
|
+
.with(TestProduct, {
|
|
438
|
+
filters: [Query.filter('inStock', FilterOp.EQ, true)]
|
|
439
|
+
})
|
|
440
|
+
.sortBy(TestUser, 'age', 'DESC')
|
|
441
|
+
.take(20)
|
|
442
|
+
.explainAnalyze();
|
|
443
|
+
|
|
444
|
+
const analysis = analyzeQueryPlan(plan);
|
|
445
|
+
|
|
446
|
+
console.log('\n=== Full Complexity Query ===');
|
|
447
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
448
|
+
console.log(`Seq Scan: ${analysis.hasSeqScan}, Index Scan: ${analysis.hasIndexScan}`);
|
|
449
|
+
console.log(`Nested Loop: ${analysis.hasNestedLoop}, Hash Join: ${analysis.hasHashJoin}`);
|
|
450
|
+
if (analysis.warnings.length > 0) {
|
|
451
|
+
console.log('Warnings:', analysis.warnings);
|
|
452
|
+
}
|
|
453
|
+
console.log('\nFull Plan:\n' + plan);
|
|
454
|
+
|
|
455
|
+
expect(analysis.executionTimeMs).toBeLessThan(1000);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('without() exclusion query', async () => {
|
|
459
|
+
const plan = await new Query()
|
|
460
|
+
.with(TestUser)
|
|
461
|
+
.without(TestProduct)
|
|
462
|
+
.explainAnalyze();
|
|
463
|
+
|
|
464
|
+
const analysis = analyzeQueryPlan(plan);
|
|
465
|
+
|
|
466
|
+
console.log('\n=== Exclusion (without) Query ===');
|
|
467
|
+
console.log(`Planning: ${analysis.planningTimeMs}ms, Execution: ${analysis.executionTimeMs}ms`);
|
|
468
|
+
console.log(`Has NOT EXISTS or anti-join pattern: ${/NOT EXISTS|Anti/i.test(plan) || /NOT IN/i.test(plan)}`);
|
|
469
|
+
|
|
470
|
+
expect(analysis.executionTimeMs).toBeLessThan(1000);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe('Performance Summary', () => {
|
|
475
|
+
test('generate performance report', async () => {
|
|
476
|
+
const results: Array<{ name: string; planningMs: number; executionMs: number; warnings: string[]; scanType: string }> = [];
|
|
477
|
+
|
|
478
|
+
const getScanType = (a: QueryPlanAnalysis) => {
|
|
479
|
+
if (a.hasIndexScan && !a.hasSeqScan) return 'Index';
|
|
480
|
+
if (a.hasSeqScan && !a.hasIndexScan) return 'Seq';
|
|
481
|
+
if (a.hasIndexScan && a.hasSeqScan) return 'Mixed';
|
|
482
|
+
return 'N/A';
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// Simple query
|
|
486
|
+
let plan = await new Query().with(TestUser).explainAnalyze();
|
|
487
|
+
let analysis = analyzeQueryPlan(plan);
|
|
488
|
+
results.push({ name: 'Simple (1 component)', planningMs: analysis.planningTimeMs, executionMs: analysis.executionTimeMs, warnings: analysis.warnings, scanType: getScanType(analysis) });
|
|
489
|
+
|
|
490
|
+
// Filtered
|
|
491
|
+
plan = await new Query().with(TestUser, { filters: [Query.filter('age', FilterOp.GT, 30)] }).explainAnalyze();
|
|
492
|
+
analysis = analyzeQueryPlan(plan);
|
|
493
|
+
results.push({ name: 'Filtered (JSONB)', planningMs: analysis.planningTimeMs, executionMs: analysis.executionTimeMs, warnings: analysis.warnings, scanType: getScanType(analysis) });
|
|
494
|
+
|
|
495
|
+
// Multi-component
|
|
496
|
+
plan = await new Query().with(TestUser).with(TestProduct).explainAnalyze();
|
|
497
|
+
analysis = analyzeQueryPlan(plan);
|
|
498
|
+
results.push({ name: 'Multi-component (2)', planningMs: analysis.planningTimeMs, executionMs: analysis.executionTimeMs, warnings: analysis.warnings, scanType: getScanType(analysis) });
|
|
499
|
+
|
|
500
|
+
// Count
|
|
501
|
+
const countStart = performance.now();
|
|
502
|
+
const count = await new Query().with(TestUser).count();
|
|
503
|
+
const countMs = performance.now() - countStart;
|
|
504
|
+
results.push({ name: `Count (result: ${count})`, planningMs: 0, executionMs: countMs, warnings: [], scanType: 'N/A' });
|
|
505
|
+
|
|
506
|
+
// Sorted + paginated
|
|
507
|
+
plan = await new Query().with(TestUser).sortBy(TestUser, 'age').take(10).explainAnalyze();
|
|
508
|
+
analysis = analyzeQueryPlan(plan);
|
|
509
|
+
results.push({ name: 'Sorted + Paginated (10)', planningMs: analysis.planningTimeMs, executionMs: analysis.executionTimeMs, warnings: analysis.warnings, scanType: getScanType(analysis) });
|
|
510
|
+
|
|
511
|
+
// Offset pagination (worst case)
|
|
512
|
+
plan = await new Query().with(TestUser).take(10).offset(Math.floor(ENTITY_COUNT * 0.9)).explainAnalyze();
|
|
513
|
+
analysis = analyzeQueryPlan(plan);
|
|
514
|
+
results.push({ name: 'Offset pagination (90%)', planningMs: analysis.planningTimeMs, executionMs: analysis.executionTimeMs, warnings: analysis.warnings, scanType: getScanType(analysis) });
|
|
515
|
+
|
|
516
|
+
// Complex
|
|
517
|
+
plan = await new Query()
|
|
518
|
+
.with(TestUser, { filters: [Query.filter('age', FilterOp.GT, 25)] })
|
|
519
|
+
.with(TestProduct, { filters: [Query.filter('inStock', FilterOp.EQ, true)] })
|
|
520
|
+
.sortBy(TestUser, 'age', 'DESC')
|
|
521
|
+
.take(10)
|
|
522
|
+
.explainAnalyze();
|
|
523
|
+
analysis = analyzeQueryPlan(plan);
|
|
524
|
+
results.push({ name: 'Complex (multi+filter+sort)', planningMs: analysis.planningTimeMs, executionMs: analysis.executionTimeMs, warnings: analysis.warnings, scanType: getScanType(analysis) });
|
|
525
|
+
|
|
526
|
+
// Sum aggregate
|
|
527
|
+
const sumStart = performance.now();
|
|
528
|
+
const sum = await new Query()
|
|
529
|
+
.with(TestProduct, { filters: [Query.filter('name', FilterOp.LIKE, 'Performance%')] })
|
|
530
|
+
.sum(TestProduct, 'price');
|
|
531
|
+
const sumMs = performance.now() - sumStart;
|
|
532
|
+
results.push({ name: `Sum (result: ${sum})`, planningMs: 0, executionMs: sumMs, warnings: [], scanType: 'N/A' });
|
|
533
|
+
|
|
534
|
+
console.log('\n' + '='.repeat(80));
|
|
535
|
+
console.log(`PERFORMANCE SUMMARY REPORT - ${ENTITY_COUNT.toLocaleString()} entities`);
|
|
536
|
+
console.log('='.repeat(80));
|
|
537
|
+
console.log('\n| Query Type | Planning | Execution | Scan |');
|
|
538
|
+
console.log('|-------------------------------------|----------|-----------|-------|');
|
|
539
|
+
|
|
540
|
+
let totalExecution = 0;
|
|
541
|
+
for (const r of results) {
|
|
542
|
+
totalExecution += r.executionMs;
|
|
543
|
+
const planStr = r.planningMs > 0 ? `${r.planningMs.toFixed(2)}ms` : '-';
|
|
544
|
+
const execStr = r.executionMs >= 1 ? `${r.executionMs.toFixed(1)}ms` : `${(r.executionMs * 1000).toFixed(0)}µs`;
|
|
545
|
+
console.log(`| ${r.name.padEnd(35)} | ${planStr.padStart(8)} | ${execStr.padStart(9)} | ${r.scanType.padStart(5)} |`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
console.log('|-------------------------------------|----------|-----------|-------|');
|
|
549
|
+
const totalStr = totalExecution >= 1 ? `${totalExecution.toFixed(1)}ms` : `${(totalExecution * 1000).toFixed(0)}µs`;
|
|
550
|
+
console.log(`| TOTAL | | ${totalStr.padStart(9)} | |`);
|
|
551
|
+
console.log('\n' + '='.repeat(80) + '\n');
|
|
552
|
+
|
|
553
|
+
// All queries should complete in reasonable time
|
|
554
|
+
expect(totalExecution).toBeLessThan(5000);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
});
|