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.
Files changed (38) hide show
  1. package/config/cache.config.ts +2 -0
  2. package/core/ArcheType.ts +67 -34
  3. package/core/BatchLoader.ts +215 -30
  4. package/core/Entity.ts +2 -2
  5. package/core/RequestContext.ts +15 -10
  6. package/core/RequestLoaders.ts +4 -2
  7. package/core/cache/CacheFactory.ts +3 -1
  8. package/core/cache/CacheProvider.ts +1 -0
  9. package/core/cache/CacheWarmer.ts +45 -23
  10. package/core/cache/MemoryCache.ts +10 -1
  11. package/core/cache/RedisCache.ts +26 -7
  12. package/core/validateEnv.ts +8 -0
  13. package/database/DatabaseHelper.ts +113 -1
  14. package/database/index.ts +78 -45
  15. package/docs/SCALABILITY_PLAN.md +175 -0
  16. package/package.json +13 -2
  17. package/query/CTENode.ts +44 -24
  18. package/query/ComponentInclusionNode.ts +181 -91
  19. package/query/Query.ts +9 -9
  20. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +338 -0
  21. package/tests/benchmark/bunfig.toml +9 -0
  22. package/tests/benchmark/fixtures/EcommerceComponents.ts +283 -0
  23. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +301 -0
  24. package/tests/benchmark/fixtures/RelationTracker.ts +159 -0
  25. package/tests/benchmark/fixtures/index.ts +6 -0
  26. package/tests/benchmark/index.ts +22 -0
  27. package/tests/benchmark/noop-preload.ts +3 -0
  28. package/tests/benchmark/runners/BenchmarkLoader.ts +132 -0
  29. package/tests/benchmark/runners/index.ts +4 -0
  30. package/tests/benchmark/scenarios/query-benchmarks.test.ts +465 -0
  31. package/tests/benchmark/scripts/generate-db.ts +344 -0
  32. package/tests/benchmark/scripts/run-benchmarks.ts +97 -0
  33. package/tests/integration/query/Query.complexAnalysis.test.ts +557 -0
  34. package/tests/integration/query/Query.explainAnalyze.test.ts +233 -0
  35. package/tests/stress/fixtures/RealisticComponents.ts +235 -0
  36. package/tests/stress/scenarios/realistic-scenarios.test.ts +1081 -0
  37. package/tests/stress/scenarios/timeout-investigation.test.ts +522 -0
  38. 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
+ });