bunsane 0.2.4 → 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 (35) hide show
  1. package/core/ArcheType.ts +67 -34
  2. package/core/BatchLoader.ts +215 -30
  3. package/core/Entity.ts +2 -2
  4. package/core/RequestContext.ts +15 -10
  5. package/core/RequestLoaders.ts +4 -2
  6. package/core/cache/CacheProvider.ts +1 -0
  7. package/core/cache/MemoryCache.ts +10 -1
  8. package/core/cache/RedisCache.ts +16 -2
  9. package/core/validateEnv.ts +8 -0
  10. package/database/DatabaseHelper.ts +113 -1
  11. package/database/index.ts +78 -45
  12. package/docs/SCALABILITY_PLAN.md +175 -0
  13. package/package.json +13 -2
  14. package/query/CTENode.ts +44 -24
  15. package/query/ComponentInclusionNode.ts +181 -91
  16. package/query/Query.ts +9 -9
  17. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +338 -0
  18. package/tests/benchmark/bunfig.toml +9 -0
  19. package/tests/benchmark/fixtures/EcommerceComponents.ts +283 -0
  20. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +301 -0
  21. package/tests/benchmark/fixtures/RelationTracker.ts +159 -0
  22. package/tests/benchmark/fixtures/index.ts +6 -0
  23. package/tests/benchmark/index.ts +22 -0
  24. package/tests/benchmark/noop-preload.ts +3 -0
  25. package/tests/benchmark/runners/BenchmarkLoader.ts +132 -0
  26. package/tests/benchmark/runners/index.ts +4 -0
  27. package/tests/benchmark/scenarios/query-benchmarks.test.ts +465 -0
  28. package/tests/benchmark/scripts/generate-db.ts +344 -0
  29. package/tests/benchmark/scripts/run-benchmarks.ts +97 -0
  30. package/tests/integration/query/Query.complexAnalysis.test.ts +557 -0
  31. package/tests/integration/query/Query.explainAnalyze.test.ts +233 -0
  32. package/tests/stress/fixtures/RealisticComponents.ts +235 -0
  33. package/tests/stress/scenarios/realistic-scenarios.test.ts +1081 -0
  34. package/tests/stress/scenarios/timeout-investigation.test.ts +522 -0
  35. package/tests/unit/BatchLoader.test.ts +139 -25
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Integration tests for Query.explainAnalyze() and debugMode()
3
+ * Tests query plan analysis and debug output
4
+ */
5
+ import { describe, test, expect, beforeAll, beforeEach } from 'bun:test';
6
+ import { Query, FilterOp } from '../../../query/Query';
7
+ import { TestUser, TestProduct } from '../../fixtures/components';
8
+ import { createTestContext, ensureComponentsRegistered } from '../../utils';
9
+
10
+ describe('Query EXPLAIN ANALYZE', () => {
11
+ const ctx = createTestContext();
12
+
13
+ beforeAll(async () => {
14
+ await ensureComponentsRegistered(TestUser, TestProduct);
15
+ });
16
+
17
+ beforeEach(async () => {
18
+ // Create test data for queries
19
+ const entity = ctx.tracker.create();
20
+ entity.add(TestUser, { name: 'ExplainTest', email: 'explain@example.com', age: 30 });
21
+ await entity.save();
22
+ });
23
+
24
+ describe('explainAnalyze()', () => {
25
+ test('returns query execution plan', async () => {
26
+ const plan = await new Query()
27
+ .with(TestUser)
28
+ .explainAnalyze();
29
+
30
+ expect(plan).toBeDefined();
31
+ expect(typeof plan).toBe('string');
32
+ expect(plan.length).toBeGreaterThan(0);
33
+ });
34
+
35
+ test('plan contains execution timing', async () => {
36
+ const plan = await new Query()
37
+ .with(TestUser)
38
+ .explainAnalyze();
39
+
40
+ // EXPLAIN ANALYZE includes actual timing
41
+ expect(plan).toMatch(/actual time=/i);
42
+ });
43
+
44
+ test('plan contains buffer statistics by default', async () => {
45
+ const plan = await new Query()
46
+ .with(TestUser)
47
+ .explainAnalyze(true);
48
+
49
+ // BUFFERS option includes shared/local buffer info
50
+ expect(plan).toMatch(/Buffers:|shared hit|shared read/i);
51
+ });
52
+
53
+ test('plan without buffers when buffers=false', async () => {
54
+ const plan = await new Query()
55
+ .with(TestUser)
56
+ .explainAnalyze(false);
57
+
58
+ expect(plan).toBeDefined();
59
+ // Should still have timing info
60
+ expect(plan).toMatch(/actual time=/i);
61
+ });
62
+
63
+ test('explains filtered query', async () => {
64
+ const plan = await new Query()
65
+ .with(TestUser, {
66
+ filters: [Query.filter('name', FilterOp.EQ, 'ExplainTest')]
67
+ })
68
+ .explainAnalyze();
69
+
70
+ expect(plan).toBeDefined();
71
+ // Filter should show in the plan
72
+ expect(plan.length).toBeGreaterThan(0);
73
+ });
74
+
75
+ test('explains query with multiple components', async () => {
76
+ // Create entity with multiple components
77
+ const entity = ctx.tracker.create();
78
+ entity.add(TestUser, { name: 'MultiComp', email: 'multi@example.com', age: 25 });
79
+ entity.add(TestProduct, { sku: 'EXPLAIN-SKU', name: 'Explain Product', price: 100, inStock: true });
80
+ await entity.save();
81
+
82
+ const plan = await new Query()
83
+ .with(TestUser)
84
+ .with(TestProduct)
85
+ .explainAnalyze();
86
+
87
+ expect(plan).toBeDefined();
88
+ expect(plan.length).toBeGreaterThan(0);
89
+ });
90
+
91
+ test('explains query with sorting', async () => {
92
+ const plan = await new Query()
93
+ .with(TestUser)
94
+ .sortBy(TestUser, 'age', 'DESC')
95
+ .explainAnalyze();
96
+
97
+ expect(plan).toBeDefined();
98
+ // Sort should show in plan
99
+ expect(plan).toMatch(/Sort|sort/i);
100
+ });
101
+
102
+ test('explains query with limit', async () => {
103
+ const plan = await new Query()
104
+ .with(TestUser)
105
+ .take(10)
106
+ .explainAnalyze();
107
+
108
+ expect(plan).toBeDefined();
109
+ // Limit should show in plan
110
+ expect(plan).toMatch(/Limit|limit/i);
111
+ });
112
+
113
+ test('explains count query equivalent', async () => {
114
+ // explainAnalyze on a regular query shows the underlying query plan
115
+ // For count analysis, one would wrap the query differently
116
+ const plan = await new Query()
117
+ .with(TestUser)
118
+ .explainAnalyze();
119
+
120
+ expect(plan).toBeDefined();
121
+ });
122
+ });
123
+
124
+ describe('debugMode()', () => {
125
+ test('debugMode does not throw', async () => {
126
+ const results = await new Query()
127
+ .with(TestUser)
128
+ .debugMode(true)
129
+ .exec();
130
+
131
+ expect(Array.isArray(results)).toBe(true);
132
+ });
133
+
134
+ test('debugMode can be disabled', async () => {
135
+ const results = await new Query()
136
+ .with(TestUser)
137
+ .debugMode(true)
138
+ .debugMode(false)
139
+ .exec();
140
+
141
+ expect(Array.isArray(results)).toBe(true);
142
+ });
143
+
144
+ test('debugMode works with count()', async () => {
145
+ const count = await new Query()
146
+ .with(TestUser)
147
+ .debugMode(true)
148
+ .count();
149
+
150
+ expect(typeof count).toBe('number');
151
+ });
152
+
153
+ test('debugMode works with explainAnalyze()', async () => {
154
+ const plan = await new Query()
155
+ .with(TestUser)
156
+ .debugMode(true)
157
+ .explainAnalyze();
158
+
159
+ expect(plan).toBeDefined();
160
+ expect(typeof plan).toBe('string');
161
+ });
162
+
163
+ test('debugMode works with sum()', async () => {
164
+ const entity = ctx.tracker.create();
165
+ entity.add(TestProduct, { sku: 'DEBUG-SUM', name: 'Debug Sum', price: 50, inStock: true });
166
+ await entity.save();
167
+
168
+ const sum = await new Query()
169
+ .with(TestProduct)
170
+ .debugMode(true)
171
+ .sum(TestProduct, 'price');
172
+
173
+ expect(typeof sum).toBe('number');
174
+ });
175
+
176
+ test('debugMode works with average()', async () => {
177
+ const entity = ctx.tracker.create();
178
+ entity.add(TestProduct, { sku: 'DEBUG-AVG', name: 'Debug Avg', price: 75, inStock: true });
179
+ await entity.save();
180
+
181
+ const avg = await new Query()
182
+ .with(TestProduct)
183
+ .debugMode(true)
184
+ .average(TestProduct, 'price');
185
+
186
+ expect(typeof avg).toBe('number');
187
+ });
188
+ });
189
+
190
+ describe('query plan analysis patterns', () => {
191
+ test('index scan shows when filtering on indexed field', async () => {
192
+ // Create several entities to make index usage more likely
193
+ for (let i = 0; i < 10; i++) {
194
+ const entity = ctx.tracker.create();
195
+ entity.add(TestUser, {
196
+ name: `IndexTest${i}`,
197
+ email: `index${i}@example.com`,
198
+ age: 20 + i
199
+ });
200
+ await entity.save();
201
+ }
202
+
203
+ const plan = await new Query()
204
+ .with(TestUser, {
205
+ filters: [Query.filter('email', FilterOp.EQ, 'index5@example.com')]
206
+ })
207
+ .explainAnalyze();
208
+
209
+ expect(plan).toBeDefined();
210
+ // Plan should show some form of scan
211
+ expect(plan).toMatch(/Scan|scan/i);
212
+ });
213
+
214
+ test('plan shows rows estimation', async () => {
215
+ const plan = await new Query()
216
+ .with(TestUser)
217
+ .explainAnalyze();
218
+
219
+ // EXPLAIN ANALYZE shows estimated vs actual rows
220
+ expect(plan).toMatch(/rows=/i);
221
+ });
222
+
223
+ test('plan shows execution time', async () => {
224
+ const plan = await new Query()
225
+ .with(TestUser)
226
+ .take(5)
227
+ .explainAnalyze();
228
+
229
+ // Planning and execution time at the end
230
+ expect(plan).toMatch(/Planning Time:|Execution Time:/i);
231
+ });
232
+ });
233
+ });
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Realistic E-commerce Components for Stress Testing
3
+ *
4
+ * These components simulate a real-world e-commerce scenario with:
5
+ * - Products with metadata
6
+ * - Inventory tracking
7
+ * - Pricing with discounts
8
+ * - Vendor relationships
9
+ * - Analytics/metrics
10
+ */
11
+ import { BaseComponent } from '../../../core/components/BaseComponent';
12
+ import { Component, CompData } from '../../../core/components/Decorators';
13
+ import { IndexedField } from '../../../core/decorators/IndexedField';
14
+
15
+ /**
16
+ * Product component - core product information
17
+ */
18
+ @Component
19
+ export class Product extends BaseComponent {
20
+ @CompData({ indexed: true })
21
+ @IndexedField('btree')
22
+ name!: string;
23
+
24
+ @CompData({ indexed: true })
25
+ @IndexedField('btree')
26
+ sku!: string;
27
+
28
+ @CompData()
29
+ description!: string;
30
+
31
+ @CompData({ indexed: true })
32
+ @IndexedField('btree')
33
+ category!: string;
34
+
35
+ @CompData()
36
+ @IndexedField('btree')
37
+ subcategory!: string;
38
+
39
+ @CompData()
40
+ tags!: string[];
41
+
42
+ @CompData({ indexed: true })
43
+ @IndexedField('btree')
44
+ status!: 'active' | 'inactive' | 'discontinued' | 'pending';
45
+
46
+ @CompData()
47
+ @IndexedField('numeric')
48
+ rating!: number;
49
+
50
+ @CompData()
51
+ @IndexedField('numeric')
52
+ reviewCount!: number;
53
+
54
+ @CompData()
55
+ @IndexedField('btree', true)
56
+ createdAt!: Date;
57
+
58
+ @CompData()
59
+ @IndexedField('btree', true)
60
+ updatedAt!: Date;
61
+ }
62
+
63
+ /**
64
+ * Inventory component - stock tracking
65
+ */
66
+ @Component
67
+ export class Inventory extends BaseComponent {
68
+ @CompData()
69
+ @IndexedField('numeric')
70
+ quantity!: number;
71
+
72
+ @CompData()
73
+ @IndexedField('numeric')
74
+ reservedQuantity!: number;
75
+
76
+ @CompData()
77
+ @IndexedField('btree')
78
+ warehouseId!: string;
79
+
80
+ @CompData()
81
+ @IndexedField('numeric')
82
+ reorderPoint!: number;
83
+
84
+ @CompData()
85
+ @IndexedField('numeric')
86
+ maxStock!: number;
87
+
88
+ @CompData({ indexed: true })
89
+ @IndexedField('btree')
90
+ stockStatus!: 'in_stock' | 'low_stock' | 'out_of_stock' | 'backordered';
91
+
92
+ @CompData()
93
+ @IndexedField('btree', true)
94
+ lastRestocked!: Date;
95
+ }
96
+
97
+ /**
98
+ * Pricing component - price and discount information
99
+ */
100
+ @Component
101
+ export class Pricing extends BaseComponent {
102
+ @CompData()
103
+ @IndexedField('numeric')
104
+ basePrice!: number;
105
+
106
+ @CompData()
107
+ @IndexedField('numeric')
108
+ salePrice!: number;
109
+
110
+ @CompData()
111
+ @IndexedField('numeric')
112
+ costPrice!: number;
113
+
114
+ @CompData()
115
+ @IndexedField('btree')
116
+ currency!: string;
117
+
118
+ @CompData()
119
+ @IndexedField('numeric')
120
+ discountPercent!: number;
121
+
122
+ @CompData({ indexed: true })
123
+ @IndexedField('gin')
124
+ isOnSale!: boolean;
125
+
126
+ @CompData()
127
+ @IndexedField('btree', true)
128
+ saleStartDate!: Date | null;
129
+
130
+ @CompData()
131
+ @IndexedField('btree', true)
132
+ saleEndDate!: Date | null;
133
+
134
+ @CompData()
135
+ @IndexedField('numeric')
136
+ profit!: number;
137
+ }
138
+
139
+ /**
140
+ * Vendor component - supplier/seller information
141
+ */
142
+ @Component
143
+ export class Vendor extends BaseComponent {
144
+ @CompData({ indexed: true })
145
+ @IndexedField('btree')
146
+ vendorId!: string;
147
+
148
+ @CompData({ indexed: true })
149
+ @IndexedField('btree')
150
+ vendorName!: string;
151
+
152
+ @CompData()
153
+ @IndexedField('btree')
154
+ region!: string;
155
+
156
+ @CompData()
157
+ @IndexedField('numeric')
158
+ vendorRating!: number;
159
+
160
+ @CompData({ indexed: true })
161
+ @IndexedField('gin')
162
+ isVerified!: boolean;
163
+
164
+ @CompData()
165
+ @IndexedField('numeric')
166
+ totalSales!: number;
167
+
168
+ @CompData()
169
+ @IndexedField('btree')
170
+ tier!: 'bronze' | 'silver' | 'gold' | 'platinum';
171
+ }
172
+
173
+ /**
174
+ * ProductMetrics component - analytics data
175
+ */
176
+ @Component
177
+ export class ProductMetrics extends BaseComponent {
178
+ @CompData()
179
+ @IndexedField('numeric')
180
+ viewCount!: number;
181
+
182
+ @CompData()
183
+ @IndexedField('numeric')
184
+ purchaseCount!: number;
185
+
186
+ @CompData()
187
+ @IndexedField('numeric')
188
+ cartAddCount!: number;
189
+
190
+ @CompData()
191
+ @IndexedField('numeric')
192
+ wishlistCount!: number;
193
+
194
+ @CompData()
195
+ @IndexedField('numeric')
196
+ returnCount!: number;
197
+
198
+ @CompData()
199
+ @IndexedField('numeric')
200
+ conversionRate!: number;
201
+
202
+ @CompData()
203
+ @IndexedField('btree', true)
204
+ lastPurchased!: Date | null;
205
+
206
+ @CompData()
207
+ @IndexedField('btree')
208
+ popularityScore!: string;
209
+ }
210
+
211
+ // Categories for realistic data generation
212
+ export const CATEGORIES = [
213
+ 'Electronics', 'Clothing', 'Home & Garden', 'Sports', 'Books',
214
+ 'Toys', 'Beauty', 'Automotive', 'Food', 'Health'
215
+ ] as const;
216
+
217
+ export const SUBCATEGORIES: Record<string, string[]> = {
218
+ 'Electronics': ['Smartphones', 'Laptops', 'Tablets', 'Accessories', 'Audio'],
219
+ 'Clothing': ['Men', 'Women', 'Kids', 'Shoes', 'Accessories'],
220
+ 'Home & Garden': ['Furniture', 'Decor', 'Kitchen', 'Garden', 'Bedding'],
221
+ 'Sports': ['Fitness', 'Outdoor', 'Team Sports', 'Water Sports', 'Winter'],
222
+ 'Books': ['Fiction', 'Non-Fiction', 'Technical', 'Children', 'Comics'],
223
+ 'Toys': ['Action Figures', 'Board Games', 'Educational', 'Outdoor', 'Puzzles'],
224
+ 'Beauty': ['Skincare', 'Makeup', 'Haircare', 'Fragrance', 'Tools'],
225
+ 'Automotive': ['Parts', 'Accessories', 'Tools', 'Care', 'Electronics'],
226
+ 'Food': ['Snacks', 'Beverages', 'Organic', 'International', 'Specialty'],
227
+ 'Health': ['Vitamins', 'Supplements', 'Personal Care', 'Medical', 'Fitness']
228
+ };
229
+
230
+ export const REGIONS = ['North', 'South', 'East', 'West', 'Central', 'International'];
231
+ export const WAREHOUSES = ['WH-001', 'WH-002', 'WH-003', 'WH-004', 'WH-005'];
232
+ export const CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'CAD'];
233
+ export const VENDOR_TIERS = ['bronze', 'silver', 'gold', 'platinum'] as const;
234
+ export const PRODUCT_STATUSES = ['active', 'inactive', 'discontinued', 'pending'] as const;
235
+ export const STOCK_STATUSES = ['in_stock', 'low_stock', 'out_of_stock', 'backordered'] as const;