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,465 @@
1
+ /**
2
+ * Query Performance Benchmarks
3
+ *
4
+ * Tests query performance against pre-generated benchmark databases.
5
+ * Uses BENCHMARK_TIER env var to select database tier.
6
+ *
7
+ * Run:
8
+ * BENCHMARK_TIER=xs bun test tests/benchmark/scenarios/query-benchmarks.test.ts
9
+ * bun run bench:run:xs
10
+ */
11
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
12
+ import { createHash } from 'node:crypto';
13
+ import { BenchUser, BenchProduct, BenchOrder, BenchOrderItem, BenchReview } from '../fixtures/EcommerceComponents';
14
+ import { Query, FilterOp } from '../../../query/Query';
15
+ import { BenchmarkRunner, type BenchmarkResult } from '../../stress/BenchmarkRunner';
16
+ import { ComponentRegistry } from '../../../core/components';
17
+ import { getMetadataStorage } from '../../../core/metadata';
18
+
19
+ // Generate type_id same way as framework
20
+ function generateTypeId(name: string): string {
21
+ return createHash('sha256').update(name).digest('hex');
22
+ }
23
+
24
+ // Tier is set by run-benchmarks.ts wrapper
25
+ const tier = process.env.BENCHMARK_TIER || 'xs';
26
+ let runner: BenchmarkRunner;
27
+ const results: BenchmarkResult[] = [];
28
+
29
+ beforeAll(async () => {
30
+ runner = new BenchmarkRunner();
31
+
32
+ // Debug: verify environment
33
+ console.log(`[DEBUG] POSTGRES_HOST: ${process.env.POSTGRES_HOST}`);
34
+ console.log(`[DEBUG] POSTGRES_PORT: ${process.env.POSTGRES_PORT}`);
35
+ console.log(`[DEBUG] USE_PGLITE: ${process.env.USE_PGLITE}`);
36
+ console.log(`[DEBUG] BUNSANE_USE_DIRECT_PARTITION: ${process.env.BUNSANE_USE_DIRECT_PARTITION}`);
37
+
38
+ // Manually register components without triggering partition table creation
39
+ // ComponentRegistry is already the singleton instance (exported as default)
40
+ const registry = ComponentRegistry as any; // Access private members
41
+ const storage = getMetadataStorage();
42
+
43
+ const components = [
44
+ { name: 'BenchUser', ctor: BenchUser },
45
+ { name: 'BenchProduct', ctor: BenchProduct },
46
+ { name: 'BenchOrder', ctor: BenchOrder },
47
+ { name: 'BenchOrderItem', ctor: BenchOrderItem },
48
+ { name: 'BenchReview', ctor: BenchReview },
49
+ ];
50
+
51
+ for (const { name, ctor } of components) {
52
+ const typeId = generateTypeId(name);
53
+ // Register in ComponentRegistry's internal maps
54
+ registry.componentsMap.set(name, typeId);
55
+ registry.typeIdToName.set(typeId, name);
56
+ registry.typeIdToCtor.set(typeId, ctor);
57
+ // Also register in metadata storage
58
+ storage.getComponentId(name);
59
+ }
60
+
61
+ console.log(`\n=== Query Benchmarks [${tier.toUpperCase()}] ===\n`);
62
+ });
63
+
64
+ afterAll(async () => {
65
+ // Print summary
66
+ console.log('\n=== Benchmark Summary ===');
67
+ const summary = runner.getSummary();
68
+ console.log(`Passed: ${summary.passed}/${summary.total}`);
69
+
70
+ if (results.length > 0) {
71
+ console.log('\nDetailed Results:');
72
+ for (const r of results) {
73
+ const status = r.passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
74
+ console.log(` ${status} ${r.name.padEnd(40)} p95=${r.timings.p95.toFixed(1).padStart(8)}ms rows=${String(r.rowsReturned).padStart(6)}`);
75
+ }
76
+ }
77
+ });
78
+
79
+ describe(`Query Benchmarks [${tier.toUpperCase()}]`, () => {
80
+ describe('Single Component Queries', () => {
81
+ test('indexed field filter (user by status)', async () => {
82
+ const result = await runner.run(
83
+ 'indexed-filter-status',
84
+ async () => {
85
+ return await new Query()
86
+ .with(BenchUser, {
87
+ filters: [Query.filter('status', FilterOp.EQ, 'active')]
88
+ })
89
+ .take(100)
90
+ .exec();
91
+ },
92
+ { iterations: 20, targetP95: 100 }
93
+ );
94
+ results.push(result);
95
+ expect(result.passed).toBe(true);
96
+ });
97
+
98
+ test('indexed field filter (product by category)', async () => {
99
+ const result = await runner.run(
100
+ 'indexed-filter-category',
101
+ async () => {
102
+ return await new Query()
103
+ .with(BenchProduct, {
104
+ filters: [Query.filter('category', FilterOp.EQ, 'Electronics')]
105
+ })
106
+ .take(100)
107
+ .exec();
108
+ },
109
+ { iterations: 20, targetP95: 100 }
110
+ );
111
+ results.push(result);
112
+ expect(result.passed).toBe(true);
113
+ });
114
+
115
+ test('numeric range filter (product by price)', async () => {
116
+ const result = await runner.run(
117
+ 'numeric-range-price',
118
+ async () => {
119
+ return await new Query()
120
+ .with(BenchProduct, {
121
+ filters: [
122
+ Query.filter('price', FilterOp.GTE, 50),
123
+ Query.filter('price', FilterOp.LTE, 200)
124
+ ]
125
+ })
126
+ .take(100)
127
+ .exec();
128
+ },
129
+ { iterations: 20, targetP95: 100 }
130
+ );
131
+ results.push(result);
132
+ expect(result.passed).toBe(true);
133
+ });
134
+
135
+ test('sorting with pagination (products by rating DESC)', async () => {
136
+ const result = await runner.run(
137
+ 'sort-rating-desc',
138
+ async () => {
139
+ return await new Query()
140
+ .with(BenchProduct)
141
+ .sortBy(BenchProduct, 'rating', 'DESC')
142
+ .take(50)
143
+ .offset(100)
144
+ .exec();
145
+ },
146
+ { iterations: 20, targetP95: 150 }
147
+ );
148
+ results.push(result);
149
+ expect(result.passed).toBe(true);
150
+ });
151
+ });
152
+
153
+ describe('Multi-Component Queries', () => {
154
+ test('two components (order + order item)', async () => {
155
+ const result = await runner.run(
156
+ 'multi-2-components',
157
+ async () => {
158
+ return await new Query()
159
+ .with(BenchOrder, {
160
+ filters: [Query.filter('status', FilterOp.EQ, 'delivered')]
161
+ })
162
+ .with(BenchOrderItem)
163
+ .take(50)
164
+ .exec();
165
+ },
166
+ { iterations: 15, targetP95: 200 }
167
+ );
168
+ results.push(result);
169
+ expect(result.passed).toBe(true);
170
+ });
171
+
172
+ test('three components (user + order + item)', async () => {
173
+ const result = await runner.run(
174
+ 'multi-3-components',
175
+ async () => {
176
+ return await new Query()
177
+ .with(BenchUser, {
178
+ filters: [Query.filter('tier', FilterOp.EQ, 'premium')]
179
+ })
180
+ .with(BenchOrder)
181
+ .with(BenchOrderItem)
182
+ .take(50)
183
+ .exec();
184
+ },
185
+ { iterations: 15, targetP95: 300 }
186
+ );
187
+ results.push(result);
188
+ expect(result.passed).toBe(true);
189
+ });
190
+ });
191
+
192
+ describe('Foreign Key Relation Queries', () => {
193
+ test('orders by userId', async () => {
194
+ // First get a user ID
195
+ const users = await new Query()
196
+ .with(BenchUser, {
197
+ filters: [Query.filter('orderCount', FilterOp.GT, 0)]
198
+ })
199
+ .take(1)
200
+ .populate()
201
+ .exec();
202
+
203
+ if (users.length === 0) {
204
+ console.log('Skipping: no users with orders found');
205
+ return;
206
+ }
207
+
208
+ const userId = users[0]!.id;
209
+
210
+ const result = await runner.run(
211
+ 'fk-orders-by-user',
212
+ async () => {
213
+ return await new Query()
214
+ .with(BenchOrder, {
215
+ filters: [Query.filter('userId', FilterOp.EQ, userId)]
216
+ })
217
+ .take(100)
218
+ .exec();
219
+ },
220
+ { iterations: 20, targetP95: 100 }
221
+ );
222
+ results.push(result);
223
+ expect(result.passed).toBe(true);
224
+ });
225
+
226
+ test('reviews by productId', async () => {
227
+ // First get a product ID
228
+ const products = await new Query()
229
+ .with(BenchProduct, {
230
+ filters: [Query.filter('reviewCount', FilterOp.GT, 0)]
231
+ })
232
+ .take(1)
233
+ .populate()
234
+ .exec();
235
+
236
+ if (products.length === 0) {
237
+ console.log('Skipping: no products with reviews found');
238
+ return;
239
+ }
240
+
241
+ const productId = products[0]!.id;
242
+
243
+ const result = await runner.run(
244
+ 'fk-reviews-by-product',
245
+ async () => {
246
+ return await new Query()
247
+ .with(BenchReview, {
248
+ filters: [Query.filter('productId', FilterOp.EQ, productId)]
249
+ })
250
+ .take(100)
251
+ .exec();
252
+ },
253
+ { iterations: 20, targetP95: 100 }
254
+ );
255
+ results.push(result);
256
+ expect(result.passed).toBe(true);
257
+ });
258
+
259
+ test('order items by orderId', async () => {
260
+ // First get an order ID
261
+ const orders = await new Query()
262
+ .with(BenchOrder, {
263
+ filters: [Query.filter('itemCount', FilterOp.GT, 0)]
264
+ })
265
+ .take(1)
266
+ .populate()
267
+ .exec();
268
+
269
+ if (orders.length === 0) {
270
+ console.log('Skipping: no orders with items found');
271
+ return;
272
+ }
273
+
274
+ const orderId = orders[0]!.id;
275
+
276
+ const result = await runner.run(
277
+ 'fk-items-by-order',
278
+ async () => {
279
+ return await new Query()
280
+ .with(BenchOrderItem, {
281
+ filters: [Query.filter('orderId', FilterOp.EQ, orderId)]
282
+ })
283
+ .take(100)
284
+ .exec();
285
+ },
286
+ { iterations: 20, targetP95: 100 }
287
+ );
288
+ results.push(result);
289
+ expect(result.passed).toBe(true);
290
+ });
291
+ });
292
+
293
+ describe('Complex Queries with Sorting', () => {
294
+ test('multi-component with filter and sort', async () => {
295
+ const result = await runner.run(
296
+ 'complex-filter-sort',
297
+ async () => {
298
+ return await new Query()
299
+ .with(BenchProduct, {
300
+ filters: [
301
+ Query.filter('status', FilterOp.EQ, 'active'),
302
+ Query.filter('stock', FilterOp.GT, 10)
303
+ ]
304
+ })
305
+ .with(BenchReview)
306
+ .sortBy(BenchProduct, 'rating', 'DESC')
307
+ .take(50)
308
+ .exec();
309
+ },
310
+ { iterations: 15, targetP95: 300 }
311
+ );
312
+ results.push(result);
313
+ expect(result.passed).toBe(true);
314
+ });
315
+
316
+ test('date range with sorting', async () => {
317
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
318
+
319
+ const result = await runner.run(
320
+ 'date-range-sorted',
321
+ async () => {
322
+ return await new Query()
323
+ .with(BenchOrder, {
324
+ filters: [
325
+ Query.filter('orderedAt', FilterOp.GTE, thirtyDaysAgo.toISOString())
326
+ ]
327
+ })
328
+ .sortBy(BenchOrder, 'total', 'DESC')
329
+ .take(100)
330
+ .exec();
331
+ },
332
+ { iterations: 15, targetP95: 200 }
333
+ );
334
+ results.push(result);
335
+ expect(result.passed).toBe(true);
336
+ });
337
+ });
338
+
339
+ describe('Pagination Performance', () => {
340
+ test('deep pagination (offset 1000)', async () => {
341
+ const result = await runner.run(
342
+ 'pagination-offset-1000',
343
+ async () => {
344
+ return await new Query()
345
+ .with(BenchProduct)
346
+ .take(50)
347
+ .offset(1000)
348
+ .exec();
349
+ },
350
+ { iterations: 15, targetP95: 200 }
351
+ );
352
+ results.push(result);
353
+ expect(result.passed).toBe(true);
354
+ });
355
+
356
+ test('very deep pagination (offset 5000)', async () => {
357
+ const result = await runner.run(
358
+ 'pagination-offset-5000',
359
+ async () => {
360
+ return await new Query()
361
+ .with(BenchProduct)
362
+ .take(50)
363
+ .offset(5000)
364
+ .exec();
365
+ },
366
+ { iterations: 10, targetP95: 500 }
367
+ );
368
+ results.push(result);
369
+ // Less strict for deep pagination
370
+ expect(result.timings.p95).toBeLessThan(1000);
371
+ });
372
+ });
373
+
374
+ describe('Count and Aggregations', () => {
375
+ test('count query', async () => {
376
+ const result = await runner.run(
377
+ 'count-products',
378
+ async () => {
379
+ const count = await new Query()
380
+ .with(BenchProduct, {
381
+ filters: [Query.filter('status', FilterOp.EQ, 'active')]
382
+ })
383
+ .count();
384
+ return [{ count }];
385
+ },
386
+ { iterations: 20, targetP95: 100 }
387
+ );
388
+ results.push(result);
389
+ expect(result.passed).toBe(true);
390
+ });
391
+
392
+ test('sum aggregation', async () => {
393
+ const result = await runner.run(
394
+ 'sum-order-totals',
395
+ async () => {
396
+ const sum = await new Query()
397
+ .with(BenchOrder, {
398
+ filters: [Query.filter('status', FilterOp.EQ, 'delivered')]
399
+ })
400
+ .sum(BenchOrder, 'total');
401
+ return [{ sum }];
402
+ },
403
+ { iterations: 20, targetP95: 150 }
404
+ );
405
+ results.push(result);
406
+ expect(result.passed).toBe(true);
407
+ });
408
+
409
+ test('average aggregation', async () => {
410
+ const result = await runner.run(
411
+ 'avg-product-price',
412
+ async () => {
413
+ const avg = await new Query()
414
+ .with(BenchProduct, {
415
+ filters: [Query.filter('category', FilterOp.EQ, 'Electronics')]
416
+ })
417
+ .average(BenchProduct, 'price');
418
+ return [{ avg }];
419
+ },
420
+ { iterations: 20, targetP95: 150 }
421
+ );
422
+ results.push(result);
423
+ expect(result.passed).toBe(true);
424
+ });
425
+ });
426
+
427
+ describe('Populate Performance', () => {
428
+ test('populate single component', async () => {
429
+ const result = await runner.run(
430
+ 'populate-single',
431
+ async () => {
432
+ return await new Query()
433
+ .with(BenchUser, {
434
+ filters: [Query.filter('tier', FilterOp.EQ, 'premium')]
435
+ })
436
+ .populate()
437
+ .take(50)
438
+ .exec();
439
+ },
440
+ { iterations: 15, targetP95: 200 }
441
+ );
442
+ results.push(result);
443
+ expect(result.passed).toBe(true);
444
+ });
445
+
446
+ test('populate multi-component', async () => {
447
+ const result = await runner.run(
448
+ 'populate-multi',
449
+ async () => {
450
+ return await new Query()
451
+ .with(BenchProduct, {
452
+ filters: [Query.filter('status', FilterOp.EQ, 'active')]
453
+ })
454
+ .with(BenchReview)
455
+ .populate()
456
+ .take(30)
457
+ .exec();
458
+ },
459
+ { iterations: 10, targetP95: 500 }
460
+ );
461
+ results.push(result);
462
+ expect(result.passed).toBe(true);
463
+ });
464
+ });
465
+ });