bunsane 0.2.4 → 0.2.8
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/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/CacheProvider.ts +1 -0
- package/core/cache/MemoryCache.ts +10 -1
- package/core/cache/RedisCache.ts +16 -2
- 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 +195 -95
- 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/query-lateral-benchmark.test.ts +372 -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.edgeCases.test.ts +595 -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,1081 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realistic Stress Test Scenarios
|
|
3
|
+
*
|
|
4
|
+
* Simulates real-world e-commerce query patterns with:
|
|
5
|
+
* - 1000+ entities seeded with multiple components
|
|
6
|
+
* - Multi-filter queries
|
|
7
|
+
* - Multi-component joins
|
|
8
|
+
* - OR queries
|
|
9
|
+
* - Aggregations
|
|
10
|
+
* - Complex sorting and pagination
|
|
11
|
+
*
|
|
12
|
+
* Run with: bun run test:pglite -- tests/stress/scenarios/realistic-scenarios.test.ts
|
|
13
|
+
* Configure: STRESS_ENTITY_COUNT=5000 for larger tests
|
|
14
|
+
*/
|
|
15
|
+
import { describe, test, beforeAll, afterAll, expect } from 'bun:test';
|
|
16
|
+
import { DataSeeder } from '../DataSeeder';
|
|
17
|
+
import { BenchmarkRunner } from '../BenchmarkRunner';
|
|
18
|
+
import { StressTestReporter } from '../StressTestReporter';
|
|
19
|
+
import { Query, FilterOp } from '../../../query/Query';
|
|
20
|
+
import { OrQuery } from '../../../query/OrQuery';
|
|
21
|
+
import {
|
|
22
|
+
Product,
|
|
23
|
+
Inventory,
|
|
24
|
+
Pricing,
|
|
25
|
+
Vendor,
|
|
26
|
+
ProductMetrics,
|
|
27
|
+
CATEGORIES,
|
|
28
|
+
SUBCATEGORIES,
|
|
29
|
+
REGIONS,
|
|
30
|
+
WAREHOUSES,
|
|
31
|
+
CURRENCIES,
|
|
32
|
+
VENDOR_TIERS,
|
|
33
|
+
PRODUCT_STATUSES,
|
|
34
|
+
STOCK_STATUSES
|
|
35
|
+
} from '../fixtures/RealisticComponents';
|
|
36
|
+
import { ensureComponentsRegistered } from '../../utils';
|
|
37
|
+
|
|
38
|
+
// Configuration via environment variables
|
|
39
|
+
const ENTITY_COUNT = parseInt(process.env.STRESS_ENTITY_COUNT || '1000', 10);
|
|
40
|
+
const BATCH_SIZE = Math.min(500, Math.floor(ENTITY_COUNT / 10) || 100);
|
|
41
|
+
|
|
42
|
+
// Helper to generate realistic test data
|
|
43
|
+
function generateProductData(index: number): Record<string, any> {
|
|
44
|
+
const category = CATEGORIES[index % CATEGORIES.length];
|
|
45
|
+
const subcategories = SUBCATEGORIES[category];
|
|
46
|
+
const subcategory = subcategories[index % subcategories.length];
|
|
47
|
+
const status = PRODUCT_STATUSES[index % PRODUCT_STATUSES.length];
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const createdDaysAgo = Math.floor(Math.random() * 365);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
name: `Product ${index} - ${subcategory}`,
|
|
53
|
+
sku: `SKU-${String(index).padStart(6, '0')}`,
|
|
54
|
+
description: `This is a ${subcategory.toLowerCase()} product in the ${category} category. High quality item with great reviews.`,
|
|
55
|
+
category,
|
|
56
|
+
subcategory,
|
|
57
|
+
tags: [category.toLowerCase(), subcategory.toLowerCase(), `tag${index % 10}`],
|
|
58
|
+
status,
|
|
59
|
+
rating: 1 + (Math.random() * 4),
|
|
60
|
+
reviewCount: Math.floor(Math.random() * 500),
|
|
61
|
+
createdAt: new Date(now.getTime() - createdDaysAgo * 24 * 60 * 60 * 1000),
|
|
62
|
+
updatedAt: new Date(now.getTime() - Math.floor(createdDaysAgo / 2) * 24 * 60 * 60 * 1000)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function generateInventoryData(index: number): Record<string, any> {
|
|
67
|
+
const quantity = Math.floor(Math.random() * 1000);
|
|
68
|
+
const reservedQuantity = Math.floor(quantity * Math.random() * 0.3);
|
|
69
|
+
const reorderPoint = 10 + Math.floor(Math.random() * 40);
|
|
70
|
+
|
|
71
|
+
let stockStatus: string;
|
|
72
|
+
if (quantity === 0) stockStatus = 'out_of_stock';
|
|
73
|
+
else if (quantity < reorderPoint) stockStatus = 'low_stock';
|
|
74
|
+
else if (index % 20 === 0) stockStatus = 'backordered';
|
|
75
|
+
else stockStatus = 'in_stock';
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
quantity,
|
|
79
|
+
reservedQuantity,
|
|
80
|
+
warehouseId: WAREHOUSES[index % WAREHOUSES.length],
|
|
81
|
+
reorderPoint,
|
|
82
|
+
maxStock: 500 + Math.floor(Math.random() * 500),
|
|
83
|
+
stockStatus,
|
|
84
|
+
lastRestocked: new Date(Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function generatePricingData(index: number): Record<string, any> {
|
|
89
|
+
const basePrice = 10 + Math.random() * 990;
|
|
90
|
+
const costPrice = basePrice * (0.3 + Math.random() * 0.4);
|
|
91
|
+
const isOnSale = index % 5 === 0;
|
|
92
|
+
const discountPercent = isOnSale ? 10 + Math.floor(Math.random() * 40) : 0;
|
|
93
|
+
const salePrice = isOnSale ? basePrice * (1 - discountPercent / 100) : basePrice;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
basePrice,
|
|
97
|
+
salePrice,
|
|
98
|
+
costPrice,
|
|
99
|
+
currency: CURRENCIES[index % CURRENCIES.length],
|
|
100
|
+
discountPercent,
|
|
101
|
+
isOnSale,
|
|
102
|
+
saleStartDate: isOnSale ? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) : null,
|
|
103
|
+
saleEndDate: isOnSale ? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) : null,
|
|
104
|
+
profit: salePrice - costPrice
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function generateVendorData(index: number): Record<string, any> {
|
|
109
|
+
const vendorIndex = index % 50;
|
|
110
|
+
return {
|
|
111
|
+
vendorId: `VENDOR-${String(vendorIndex).padStart(3, '0')}`,
|
|
112
|
+
vendorName: `Vendor ${vendorIndex} Inc.`,
|
|
113
|
+
region: REGIONS[vendorIndex % REGIONS.length],
|
|
114
|
+
vendorRating: 3 + Math.random() * 2,
|
|
115
|
+
isVerified: vendorIndex % 3 !== 0,
|
|
116
|
+
totalSales: Math.floor(Math.random() * 100000),
|
|
117
|
+
tier: VENDOR_TIERS[Math.floor(vendorIndex / 12.5)]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function generateMetricsData(index: number): Record<string, any> {
|
|
122
|
+
const viewCount = Math.floor(Math.random() * 10000);
|
|
123
|
+
const cartAddCount = Math.floor(viewCount * (0.1 + Math.random() * 0.2));
|
|
124
|
+
const purchaseCount = Math.floor(cartAddCount * (0.2 + Math.random() * 0.3));
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
viewCount,
|
|
128
|
+
purchaseCount,
|
|
129
|
+
cartAddCount,
|
|
130
|
+
wishlistCount: Math.floor(Math.random() * 500),
|
|
131
|
+
returnCount: Math.floor(purchaseCount * Math.random() * 0.1),
|
|
132
|
+
conversionRate: purchaseCount / (viewCount || 1),
|
|
133
|
+
lastPurchased: purchaseCount > 0
|
|
134
|
+
? new Date(Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000)
|
|
135
|
+
: null,
|
|
136
|
+
popularityScore: viewCount > 5000 ? 'high' : viewCount > 1000 ? 'medium' : 'low'
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe('Realistic E-Commerce Stress Tests', () => {
|
|
141
|
+
const seeder = new DataSeeder();
|
|
142
|
+
const benchmark = new BenchmarkRunner();
|
|
143
|
+
const reporter = new StressTestReporter();
|
|
144
|
+
let entityIds: string[] = [];
|
|
145
|
+
let setupTime = 0;
|
|
146
|
+
|
|
147
|
+
beforeAll(async () => {
|
|
148
|
+
const startSetup = performance.now();
|
|
149
|
+
|
|
150
|
+
console.log(`\n Registering components...`);
|
|
151
|
+
await ensureComponentsRegistered(
|
|
152
|
+
Product,
|
|
153
|
+
Inventory,
|
|
154
|
+
Pricing,
|
|
155
|
+
Vendor,
|
|
156
|
+
ProductMetrics
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Allow index creation to settle
|
|
160
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
161
|
+
|
|
162
|
+
console.log(` Seeding ${ENTITY_COUNT.toLocaleString()} product entities...`);
|
|
163
|
+
|
|
164
|
+
// Seed primary Product component
|
|
165
|
+
const result = await seeder.seed(
|
|
166
|
+
Product,
|
|
167
|
+
generateProductData,
|
|
168
|
+
{
|
|
169
|
+
totalEntities: ENTITY_COUNT,
|
|
170
|
+
batchSize: BATCH_SIZE,
|
|
171
|
+
onProgress: (current, total, elapsed) => {
|
|
172
|
+
if (current % (BATCH_SIZE * 5) === 0 || current === total) {
|
|
173
|
+
const pct = ((current / total) * 100).toFixed(1);
|
|
174
|
+
const rate = ((current / elapsed) * 1000).toFixed(0);
|
|
175
|
+
console.log(` Product: ${pct}% (${rate}/sec)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
entityIds = result.entityIds;
|
|
181
|
+
console.log(` Products seeded: ${result.recordsPerSecond.toFixed(0)}/sec`);
|
|
182
|
+
|
|
183
|
+
// Add Inventory to all products
|
|
184
|
+
console.log(` Adding Inventory components...`);
|
|
185
|
+
await seeder.seedAdditionalComponent(
|
|
186
|
+
entityIds,
|
|
187
|
+
Inventory,
|
|
188
|
+
generateInventoryData,
|
|
189
|
+
BATCH_SIZE
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Add Pricing to all products
|
|
193
|
+
console.log(` Adding Pricing components...`);
|
|
194
|
+
await seeder.seedAdditionalComponent(
|
|
195
|
+
entityIds,
|
|
196
|
+
Pricing,
|
|
197
|
+
generatePricingData,
|
|
198
|
+
BATCH_SIZE
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Add Vendor to 80% of products
|
|
202
|
+
const vendorEntityIds = entityIds.slice(0, Math.floor(entityIds.length * 0.8));
|
|
203
|
+
console.log(` Adding Vendor components to ${vendorEntityIds.length} products...`);
|
|
204
|
+
await seeder.seedAdditionalComponent(
|
|
205
|
+
vendorEntityIds,
|
|
206
|
+
Vendor,
|
|
207
|
+
generateVendorData,
|
|
208
|
+
BATCH_SIZE
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Add ProductMetrics to 60% of products
|
|
212
|
+
const metricsEntityIds = entityIds.slice(0, Math.floor(entityIds.length * 0.6));
|
|
213
|
+
console.log(` Adding ProductMetrics to ${metricsEntityIds.length} products...`);
|
|
214
|
+
await seeder.seedAdditionalComponent(
|
|
215
|
+
metricsEntityIds,
|
|
216
|
+
ProductMetrics,
|
|
217
|
+
generateMetricsData,
|
|
218
|
+
BATCH_SIZE
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
console.log(' Running VACUUM ANALYZE...');
|
|
222
|
+
await seeder.optimize();
|
|
223
|
+
|
|
224
|
+
setupTime = performance.now() - startSetup;
|
|
225
|
+
console.log(` Setup complete in ${(setupTime / 1000).toFixed(1)}s\n`);
|
|
226
|
+
}, 120000);
|
|
227
|
+
|
|
228
|
+
afterAll(async () => {
|
|
229
|
+
// Print report
|
|
230
|
+
const recordCount = await seeder.getRecordCount();
|
|
231
|
+
const report = reporter.generateReport(benchmark.getResults(), {
|
|
232
|
+
recordCount,
|
|
233
|
+
environment: `PGlite/PostgreSQL, Bun ${Bun.version}`,
|
|
234
|
+
duration: setupTime
|
|
235
|
+
});
|
|
236
|
+
console.log('\n' + report);
|
|
237
|
+
|
|
238
|
+
// Cleanup
|
|
239
|
+
console.log('\n Cleaning up test data...');
|
|
240
|
+
await seeder.cleanup(entityIds, BATCH_SIZE);
|
|
241
|
+
console.log(' Cleanup complete.');
|
|
242
|
+
}, 60000);
|
|
243
|
+
|
|
244
|
+
// ============================================================
|
|
245
|
+
// SINGLE COMPONENT FILTER TESTS
|
|
246
|
+
// ============================================================
|
|
247
|
+
|
|
248
|
+
describe('Single Component Filters', () => {
|
|
249
|
+
test('filter by category (indexed)', async () => {
|
|
250
|
+
const result = await benchmark.runWithOutput(
|
|
251
|
+
'Product: category=Electronics',
|
|
252
|
+
() => new Query()
|
|
253
|
+
.with(Product, {
|
|
254
|
+
filters: [{ field: 'category', operator: FilterOp.EQ, value: 'Electronics' }]
|
|
255
|
+
})
|
|
256
|
+
.take(100)
|
|
257
|
+
.exec(),
|
|
258
|
+
{ targetP95: 100, iterations: 10 }
|
|
259
|
+
);
|
|
260
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
261
|
+
expect(result.passed).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('filter by status (indexed)', async () => {
|
|
265
|
+
const result = await benchmark.runWithOutput(
|
|
266
|
+
'Product: status=active',
|
|
267
|
+
() => new Query()
|
|
268
|
+
.with(Product, {
|
|
269
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
270
|
+
})
|
|
271
|
+
.take(100)
|
|
272
|
+
.exec(),
|
|
273
|
+
{ targetP95: 100, iterations: 10 }
|
|
274
|
+
);
|
|
275
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('filter by rating range', async () => {
|
|
279
|
+
const result = await benchmark.runWithOutput(
|
|
280
|
+
'Product: rating >= 4',
|
|
281
|
+
() => new Query()
|
|
282
|
+
.with(Product, {
|
|
283
|
+
filters: [{ field: 'rating', operator: FilterOp.GTE, value: 4 }]
|
|
284
|
+
})
|
|
285
|
+
.take(100)
|
|
286
|
+
.exec(),
|
|
287
|
+
{ targetP95: 100, iterations: 10 }
|
|
288
|
+
);
|
|
289
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('filter by stock status', async () => {
|
|
293
|
+
const result = await benchmark.runWithOutput(
|
|
294
|
+
'Inventory: stockStatus=in_stock',
|
|
295
|
+
() => new Query()
|
|
296
|
+
.with(Inventory, {
|
|
297
|
+
filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
|
|
298
|
+
})
|
|
299
|
+
.take(100)
|
|
300
|
+
.exec(),
|
|
301
|
+
{ targetP95: 100, iterations: 10 }
|
|
302
|
+
);
|
|
303
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('filter by price range', async () => {
|
|
307
|
+
const result = await benchmark.runWithOutput(
|
|
308
|
+
'Pricing: 50 <= basePrice <= 200',
|
|
309
|
+
() => new Query()
|
|
310
|
+
.with(Pricing, {
|
|
311
|
+
filters: [
|
|
312
|
+
{ field: 'basePrice', operator: FilterOp.GTE, value: 50 },
|
|
313
|
+
{ field: 'basePrice', operator: FilterOp.LTE, value: 200 }
|
|
314
|
+
]
|
|
315
|
+
})
|
|
316
|
+
.take(100)
|
|
317
|
+
.exec(),
|
|
318
|
+
{ targetP95: 100, iterations: 10 }
|
|
319
|
+
);
|
|
320
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('filter by boolean (isOnSale)', async () => {
|
|
324
|
+
const result = await benchmark.runWithOutput(
|
|
325
|
+
'Pricing: isOnSale=true',
|
|
326
|
+
() => new Query()
|
|
327
|
+
.with(Pricing, {
|
|
328
|
+
filters: [{ field: 'isOnSale', operator: FilterOp.EQ, value: true }]
|
|
329
|
+
})
|
|
330
|
+
.take(100)
|
|
331
|
+
.exec(),
|
|
332
|
+
{ targetP95: 100, iterations: 10 }
|
|
333
|
+
);
|
|
334
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ============================================================
|
|
339
|
+
// MULTI-FILTER SINGLE COMPONENT TESTS
|
|
340
|
+
// ============================================================
|
|
341
|
+
|
|
342
|
+
describe('Multi-Filter Single Component', () => {
|
|
343
|
+
test('active products in Electronics with high rating', async () => {
|
|
344
|
+
const result = await benchmark.runWithOutput(
|
|
345
|
+
'Product: active + Electronics + rating>=4',
|
|
346
|
+
() => new Query()
|
|
347
|
+
.with(Product, {
|
|
348
|
+
filters: [
|
|
349
|
+
{ field: 'status', operator: FilterOp.EQ, value: 'active' },
|
|
350
|
+
{ field: 'category', operator: FilterOp.EQ, value: 'Electronics' },
|
|
351
|
+
{ field: 'rating', operator: FilterOp.GTE, value: 4 }
|
|
352
|
+
]
|
|
353
|
+
})
|
|
354
|
+
.take(50)
|
|
355
|
+
.exec(),
|
|
356
|
+
{ targetP95: 150, iterations: 10 }
|
|
357
|
+
);
|
|
358
|
+
expect(result.passed).toBe(true);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('low stock items below reorder point', async () => {
|
|
362
|
+
const result = await benchmark.runWithOutput(
|
|
363
|
+
'Inventory: low_stock + qty < 20',
|
|
364
|
+
() => new Query()
|
|
365
|
+
.with(Inventory, {
|
|
366
|
+
filters: [
|
|
367
|
+
{ field: 'stockStatus', operator: FilterOp.EQ, value: 'low_stock' },
|
|
368
|
+
{ field: 'quantity', operator: FilterOp.LT, value: 20 }
|
|
369
|
+
]
|
|
370
|
+
})
|
|
371
|
+
.take(50)
|
|
372
|
+
.exec(),
|
|
373
|
+
{ targetP95: 150, iterations: 10 }
|
|
374
|
+
);
|
|
375
|
+
expect(result.passed).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('high margin sale items', async () => {
|
|
379
|
+
const result = await benchmark.runWithOutput(
|
|
380
|
+
'Pricing: onSale + profit > 100',
|
|
381
|
+
() => new Query()
|
|
382
|
+
.with(Pricing, {
|
|
383
|
+
filters: [
|
|
384
|
+
{ field: 'isOnSale', operator: FilterOp.EQ, value: true },
|
|
385
|
+
{ field: 'profit', operator: FilterOp.GT, value: 100 }
|
|
386
|
+
]
|
|
387
|
+
})
|
|
388
|
+
.take(50)
|
|
389
|
+
.exec(),
|
|
390
|
+
{ targetP95: 150, iterations: 10 }
|
|
391
|
+
);
|
|
392
|
+
expect(result.passed).toBe(true);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test('verified gold/platinum vendors with high rating', async () => {
|
|
396
|
+
const result = await benchmark.runWithOutput(
|
|
397
|
+
'Vendor: verified + rating>=4 + tier=gold',
|
|
398
|
+
() => new Query()
|
|
399
|
+
.with(Vendor, {
|
|
400
|
+
filters: [
|
|
401
|
+
{ field: 'isVerified', operator: FilterOp.EQ, value: true },
|
|
402
|
+
{ field: 'vendorRating', operator: FilterOp.GTE, value: 4 },
|
|
403
|
+
{ field: 'tier', operator: FilterOp.EQ, value: 'gold' }
|
|
404
|
+
]
|
|
405
|
+
})
|
|
406
|
+
.take(50)
|
|
407
|
+
.exec(),
|
|
408
|
+
{ targetP95: 150, iterations: 10 }
|
|
409
|
+
);
|
|
410
|
+
expect(result.passed).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// ============================================================
|
|
415
|
+
// MULTI-COMPONENT JOIN TESTS
|
|
416
|
+
// ============================================================
|
|
417
|
+
|
|
418
|
+
describe('Multi-Component Joins', () => {
|
|
419
|
+
test('2-way join: Product + Inventory', async () => {
|
|
420
|
+
const result = await benchmark.runWithOutput(
|
|
421
|
+
'Join: Product + Inventory',
|
|
422
|
+
() => new Query()
|
|
423
|
+
.with(Product)
|
|
424
|
+
.with(Inventory)
|
|
425
|
+
.take(100)
|
|
426
|
+
.exec(),
|
|
427
|
+
{ targetP95: 150, iterations: 10 }
|
|
428
|
+
);
|
|
429
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
430
|
+
expect(result.passed).toBe(true);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('3-way join: Product + Inventory + Pricing', async () => {
|
|
434
|
+
const result = await benchmark.runWithOutput(
|
|
435
|
+
'Join: Product + Inventory + Pricing',
|
|
436
|
+
() => new Query()
|
|
437
|
+
.with(Product)
|
|
438
|
+
.with(Inventory)
|
|
439
|
+
.with(Pricing)
|
|
440
|
+
.take(100)
|
|
441
|
+
.exec(),
|
|
442
|
+
{ targetP95: 200, iterations: 10 }
|
|
443
|
+
);
|
|
444
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('4-way join: Product + Inventory + Pricing + Vendor', async () => {
|
|
448
|
+
const result = await benchmark.runWithOutput(
|
|
449
|
+
'Join: Product + Inventory + Pricing + Vendor',
|
|
450
|
+
() => new Query()
|
|
451
|
+
.with(Product)
|
|
452
|
+
.with(Inventory)
|
|
453
|
+
.with(Pricing)
|
|
454
|
+
.with(Vendor)
|
|
455
|
+
.take(100)
|
|
456
|
+
.exec(),
|
|
457
|
+
{ targetP95: 300, iterations: 10 }
|
|
458
|
+
);
|
|
459
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('5-way join (all components)', async () => {
|
|
463
|
+
const result = await benchmark.runWithOutput(
|
|
464
|
+
'Join: All 5 components',
|
|
465
|
+
() => new Query()
|
|
466
|
+
.with(Product)
|
|
467
|
+
.with(Inventory)
|
|
468
|
+
.with(Pricing)
|
|
469
|
+
.with(Vendor)
|
|
470
|
+
.with(ProductMetrics)
|
|
471
|
+
.take(100)
|
|
472
|
+
.exec(),
|
|
473
|
+
{ targetP95: 400, iterations: 10 }
|
|
474
|
+
);
|
|
475
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ============================================================
|
|
480
|
+
// MULTI-COMPONENT WITH FILTERS TESTS
|
|
481
|
+
// ============================================================
|
|
482
|
+
|
|
483
|
+
describe('Multi-Component with Filters', () => {
|
|
484
|
+
test('active Electronics in stock', async () => {
|
|
485
|
+
const result = await benchmark.runWithOutput(
|
|
486
|
+
'Product(active+Electronics) + Inventory(in_stock)',
|
|
487
|
+
() => new Query()
|
|
488
|
+
.with(Product, {
|
|
489
|
+
filters: [
|
|
490
|
+
{ field: 'status', operator: FilterOp.EQ, value: 'active' },
|
|
491
|
+
{ field: 'category', operator: FilterOp.EQ, value: 'Electronics' }
|
|
492
|
+
]
|
|
493
|
+
})
|
|
494
|
+
.with(Inventory, {
|
|
495
|
+
filters: [
|
|
496
|
+
{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }
|
|
497
|
+
]
|
|
498
|
+
})
|
|
499
|
+
.take(50)
|
|
500
|
+
.exec(),
|
|
501
|
+
{ targetP95: 200, iterations: 10 }
|
|
502
|
+
);
|
|
503
|
+
expect(result.passed).toBe(true);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('on-sale products with low stock', async () => {
|
|
507
|
+
const result = await benchmark.runWithOutput(
|
|
508
|
+
'Pricing(onSale) + Inventory(low_stock)',
|
|
509
|
+
() => new Query()
|
|
510
|
+
.with(Pricing, {
|
|
511
|
+
filters: [{ field: 'isOnSale', operator: FilterOp.EQ, value: true }]
|
|
512
|
+
})
|
|
513
|
+
.with(Inventory, {
|
|
514
|
+
filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'low_stock' }]
|
|
515
|
+
})
|
|
516
|
+
.take(50)
|
|
517
|
+
.exec(),
|
|
518
|
+
{ targetP95: 200, iterations: 10 }
|
|
519
|
+
);
|
|
520
|
+
expect(result.passed).toBe(true);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('high-rated products from verified vendors', async () => {
|
|
524
|
+
const result = await benchmark.runWithOutput(
|
|
525
|
+
'Product(rating>=4) + Vendor(verified)',
|
|
526
|
+
() => new Query()
|
|
527
|
+
.with(Product, {
|
|
528
|
+
filters: [{ field: 'rating', operator: FilterOp.GTE, value: 4 }]
|
|
529
|
+
})
|
|
530
|
+
.with(Vendor, {
|
|
531
|
+
filters: [{ field: 'isVerified', operator: FilterOp.EQ, value: true }]
|
|
532
|
+
})
|
|
533
|
+
.take(50)
|
|
534
|
+
.exec(),
|
|
535
|
+
{ targetP95: 200, iterations: 10 }
|
|
536
|
+
);
|
|
537
|
+
expect(result.passed).toBe(true);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test('complex: active, in stock, on sale, price range', async () => {
|
|
541
|
+
const result = await benchmark.runWithOutput(
|
|
542
|
+
'Complex 4-filter multi-component',
|
|
543
|
+
() => new Query()
|
|
544
|
+
.with(Product, {
|
|
545
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
546
|
+
})
|
|
547
|
+
.with(Inventory, {
|
|
548
|
+
filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
|
|
549
|
+
})
|
|
550
|
+
.with(Pricing, {
|
|
551
|
+
filters: [
|
|
552
|
+
{ field: 'isOnSale', operator: FilterOp.EQ, value: true },
|
|
553
|
+
{ field: 'salePrice', operator: FilterOp.LTE, value: 500 }
|
|
554
|
+
]
|
|
555
|
+
})
|
|
556
|
+
.take(50)
|
|
557
|
+
.exec(),
|
|
558
|
+
{ targetP95: 300, iterations: 10 }
|
|
559
|
+
);
|
|
560
|
+
expect(result.passed).toBe(true);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('full pipeline: active + in_stock + onSale + verified + high popularity', async () => {
|
|
564
|
+
const result = await benchmark.runWithOutput(
|
|
565
|
+
'Full pipeline: 5-component filtered',
|
|
566
|
+
() => new Query()
|
|
567
|
+
.with(Product, {
|
|
568
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
569
|
+
})
|
|
570
|
+
.with(Inventory, {
|
|
571
|
+
filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
|
|
572
|
+
})
|
|
573
|
+
.with(Pricing, {
|
|
574
|
+
filters: [{ field: 'isOnSale', operator: FilterOp.EQ, value: true }]
|
|
575
|
+
})
|
|
576
|
+
.with(Vendor, {
|
|
577
|
+
filters: [{ field: 'isVerified', operator: FilterOp.EQ, value: true }]
|
|
578
|
+
})
|
|
579
|
+
.with(ProductMetrics, {
|
|
580
|
+
filters: [{ field: 'popularityScore', operator: FilterOp.EQ, value: 'high' }]
|
|
581
|
+
})
|
|
582
|
+
.take(50)
|
|
583
|
+
.exec(),
|
|
584
|
+
{ targetP95: 500, iterations: 10 }
|
|
585
|
+
);
|
|
586
|
+
expect(result.passed).toBe(true);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// ============================================================
|
|
591
|
+
// OR QUERY TESTS
|
|
592
|
+
// ============================================================
|
|
593
|
+
|
|
594
|
+
describe('OR Queries', () => {
|
|
595
|
+
test('OR: active OR pending products', async () => {
|
|
596
|
+
const orQuery = new OrQuery([
|
|
597
|
+
{ component: Product, filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }] },
|
|
598
|
+
{ component: Product, filters: [{ field: 'status', operator: FilterOp.EQ, value: 'pending' }] }
|
|
599
|
+
]);
|
|
600
|
+
|
|
601
|
+
const result = await benchmark.runWithOutput(
|
|
602
|
+
'OR: status=active OR status=pending',
|
|
603
|
+
() => new Query()
|
|
604
|
+
.with(orQuery)
|
|
605
|
+
.take(100)
|
|
606
|
+
.exec(),
|
|
607
|
+
{ targetP95: 200, iterations: 10 }
|
|
608
|
+
);
|
|
609
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test('OR: low_stock OR out_of_stock', async () => {
|
|
613
|
+
const orQuery = new OrQuery([
|
|
614
|
+
{ component: Inventory, filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'low_stock' }] },
|
|
615
|
+
{ component: Inventory, filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'out_of_stock' }] }
|
|
616
|
+
]);
|
|
617
|
+
|
|
618
|
+
const result = await benchmark.runWithOutput(
|
|
619
|
+
'OR: low_stock OR out_of_stock',
|
|
620
|
+
() => new Query()
|
|
621
|
+
.with(orQuery)
|
|
622
|
+
.take(100)
|
|
623
|
+
.exec(),
|
|
624
|
+
{ targetP95: 200, iterations: 10 }
|
|
625
|
+
);
|
|
626
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test('OR: Electronics OR Clothing categories', async () => {
|
|
630
|
+
const orQuery = new OrQuery([
|
|
631
|
+
{ component: Product, filters: [{ field: 'category', operator: FilterOp.EQ, value: 'Electronics' }] },
|
|
632
|
+
{ component: Product, filters: [{ field: 'category', operator: FilterOp.EQ, value: 'Clothing' }] }
|
|
633
|
+
]);
|
|
634
|
+
|
|
635
|
+
const result = await benchmark.runWithOutput(
|
|
636
|
+
'OR: Electronics OR Clothing',
|
|
637
|
+
() => new Query()
|
|
638
|
+
.with(orQuery)
|
|
639
|
+
.take(100)
|
|
640
|
+
.exec(),
|
|
641
|
+
{ targetP95: 200, iterations: 10 }
|
|
642
|
+
);
|
|
643
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// ============================================================
|
|
648
|
+
// EXCLUSION TESTS (without)
|
|
649
|
+
// ============================================================
|
|
650
|
+
|
|
651
|
+
describe('Component Exclusion (without)', () => {
|
|
652
|
+
test('products without Vendor', async () => {
|
|
653
|
+
const result = await benchmark.runWithOutput(
|
|
654
|
+
'Product without Vendor',
|
|
655
|
+
() => new Query()
|
|
656
|
+
.with(Product)
|
|
657
|
+
.without(Vendor)
|
|
658
|
+
.take(100)
|
|
659
|
+
.exec(),
|
|
660
|
+
{ targetP95: 150, iterations: 10 }
|
|
661
|
+
);
|
|
662
|
+
// ~20% of products don't have vendors
|
|
663
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('products without ProductMetrics', async () => {
|
|
667
|
+
const result = await benchmark.runWithOutput(
|
|
668
|
+
'Product without Metrics',
|
|
669
|
+
() => new Query()
|
|
670
|
+
.with(Product)
|
|
671
|
+
.without(ProductMetrics)
|
|
672
|
+
.take(100)
|
|
673
|
+
.exec(),
|
|
674
|
+
{ targetP95: 150, iterations: 10 }
|
|
675
|
+
);
|
|
676
|
+
// ~40% of products don't have metrics
|
|
677
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test('in-stock products from non-verified vendors', async () => {
|
|
681
|
+
const result = await benchmark.runWithOutput(
|
|
682
|
+
'Inventory(in_stock) + Vendor(!verified)',
|
|
683
|
+
() => new Query()
|
|
684
|
+
.with(Inventory, {
|
|
685
|
+
filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
|
|
686
|
+
})
|
|
687
|
+
.with(Vendor, {
|
|
688
|
+
filters: [{ field: 'isVerified', operator: FilterOp.EQ, value: false }]
|
|
689
|
+
})
|
|
690
|
+
.take(50)
|
|
691
|
+
.exec(),
|
|
692
|
+
{ targetP95: 200, iterations: 10 }
|
|
693
|
+
);
|
|
694
|
+
expect(result.passed).toBe(true);
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// ============================================================
|
|
699
|
+
// SORTING TESTS
|
|
700
|
+
// ============================================================
|
|
701
|
+
|
|
702
|
+
describe('Sorting', () => {
|
|
703
|
+
test('sort by rating DESC', async () => {
|
|
704
|
+
const result = await benchmark.runWithOutput(
|
|
705
|
+
'Product sorted by rating DESC',
|
|
706
|
+
() => new Query()
|
|
707
|
+
.with(Product)
|
|
708
|
+
.sortBy(Product, 'rating', 'DESC')
|
|
709
|
+
.take(100)
|
|
710
|
+
.exec(),
|
|
711
|
+
{ targetP95: 150, iterations: 10 }
|
|
712
|
+
);
|
|
713
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test('sort by basePrice ASC', async () => {
|
|
717
|
+
const result = await benchmark.runWithOutput(
|
|
718
|
+
'Pricing sorted by basePrice ASC',
|
|
719
|
+
() => new Query()
|
|
720
|
+
.with(Pricing)
|
|
721
|
+
.sortBy(Pricing, 'basePrice', 'ASC')
|
|
722
|
+
.take(100)
|
|
723
|
+
.exec(),
|
|
724
|
+
{ targetP95: 150, iterations: 10 }
|
|
725
|
+
);
|
|
726
|
+
expect(result.rowsReturned).toBeGreaterThan(0);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('filter + sort: active products by rating', async () => {
|
|
730
|
+
const result = await benchmark.runWithOutput(
|
|
731
|
+
'Product(active) sorted by rating',
|
|
732
|
+
() => new Query()
|
|
733
|
+
.with(Product, {
|
|
734
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
735
|
+
})
|
|
736
|
+
.sortBy(Product, 'rating', 'DESC')
|
|
737
|
+
.take(50)
|
|
738
|
+
.exec(),
|
|
739
|
+
{ targetP95: 200, iterations: 10 }
|
|
740
|
+
);
|
|
741
|
+
expect(result.passed).toBe(true);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test('multi-component filter + sort: on-sale by discount', async () => {
|
|
745
|
+
const result = await benchmark.runWithOutput(
|
|
746
|
+
'Pricing(onSale) sorted by discount',
|
|
747
|
+
() => new Query()
|
|
748
|
+
.with(Product, {
|
|
749
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
750
|
+
})
|
|
751
|
+
.with(Pricing, {
|
|
752
|
+
filters: [{ field: 'isOnSale', operator: FilterOp.EQ, value: true }]
|
|
753
|
+
})
|
|
754
|
+
.sortBy(Pricing, 'discountPercent', 'DESC')
|
|
755
|
+
.take(50)
|
|
756
|
+
.exec(),
|
|
757
|
+
{ targetP95: 250, iterations: 10 }
|
|
758
|
+
);
|
|
759
|
+
expect(result.passed).toBe(true);
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// ============================================================
|
|
764
|
+
// PAGINATION TESTS
|
|
765
|
+
// ============================================================
|
|
766
|
+
|
|
767
|
+
describe('Pagination', () => {
|
|
768
|
+
test('offset pagination: page 1', async () => {
|
|
769
|
+
const result = await benchmark.runWithOutput(
|
|
770
|
+
'Offset: page 1 (0-50)',
|
|
771
|
+
() => new Query()
|
|
772
|
+
.with(Product)
|
|
773
|
+
.take(50)
|
|
774
|
+
.offset(0)
|
|
775
|
+
.exec(),
|
|
776
|
+
{ targetP95: 100, iterations: 10 }
|
|
777
|
+
);
|
|
778
|
+
expect(result.rowsReturned).toBe(50);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test('offset pagination: page 10', async () => {
|
|
782
|
+
const result = await benchmark.runWithOutput(
|
|
783
|
+
'Offset: page 10 (450-500)',
|
|
784
|
+
() => new Query()
|
|
785
|
+
.with(Product)
|
|
786
|
+
.take(50)
|
|
787
|
+
.offset(450)
|
|
788
|
+
.exec(),
|
|
789
|
+
{ targetP95: 150, iterations: 10 }
|
|
790
|
+
);
|
|
791
|
+
expect(result.rowsReturned).toBeGreaterThanOrEqual(0);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test('cursor pagination: first page', async () => {
|
|
795
|
+
const result = await benchmark.runWithOutput(
|
|
796
|
+
'Cursor: first page',
|
|
797
|
+
() => new Query()
|
|
798
|
+
.with(Product)
|
|
799
|
+
.take(50)
|
|
800
|
+
.exec(),
|
|
801
|
+
{ targetP95: 100, iterations: 10 }
|
|
802
|
+
);
|
|
803
|
+
expect(result.rowsReturned).toBe(50);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test('cursor pagination: from middle', async () => {
|
|
807
|
+
// Get a cursor from the middle
|
|
808
|
+
const midpoint = await new Query()
|
|
809
|
+
.with(Product)
|
|
810
|
+
.take(1)
|
|
811
|
+
.offset(Math.floor(ENTITY_COUNT / 2))
|
|
812
|
+
.exec();
|
|
813
|
+
|
|
814
|
+
const cursorId = midpoint[0]?.id;
|
|
815
|
+
if (!cursorId) {
|
|
816
|
+
console.log(' Skipping cursor test - no midpoint found');
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const result = await benchmark.runWithOutput(
|
|
821
|
+
'Cursor: from middle',
|
|
822
|
+
() => new Query()
|
|
823
|
+
.with(Product)
|
|
824
|
+
.cursor(cursorId)
|
|
825
|
+
.take(50)
|
|
826
|
+
.exec(),
|
|
827
|
+
{ targetP95: 100, iterations: 10 }
|
|
828
|
+
);
|
|
829
|
+
expect(result.passed).toBe(true);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
test('filtered pagination: active products page 5', async () => {
|
|
833
|
+
const result = await benchmark.runWithOutput(
|
|
834
|
+
'Filtered offset: active page 5',
|
|
835
|
+
() => new Query()
|
|
836
|
+
.with(Product, {
|
|
837
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
838
|
+
})
|
|
839
|
+
.take(50)
|
|
840
|
+
.offset(200)
|
|
841
|
+
.exec(),
|
|
842
|
+
{ targetP95: 200, iterations: 10 }
|
|
843
|
+
);
|
|
844
|
+
expect(result.passed).toBe(true);
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// ============================================================
|
|
849
|
+
// AGGREGATION TESTS
|
|
850
|
+
// ============================================================
|
|
851
|
+
|
|
852
|
+
describe('Aggregations', () => {
|
|
853
|
+
test('count all products', async () => {
|
|
854
|
+
const result = await benchmark.runWithOutput(
|
|
855
|
+
'COUNT: all products',
|
|
856
|
+
async () => [await new Query().with(Product).count()],
|
|
857
|
+
{ targetP95: 100, iterations: 10 }
|
|
858
|
+
);
|
|
859
|
+
expect(result.passed).toBe(true);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
test('count filtered: active products', async () => {
|
|
863
|
+
const result = await benchmark.runWithOutput(
|
|
864
|
+
'COUNT: active products',
|
|
865
|
+
async () => [await new Query()
|
|
866
|
+
.with(Product, {
|
|
867
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
868
|
+
})
|
|
869
|
+
.count()],
|
|
870
|
+
{ targetP95: 100, iterations: 10 }
|
|
871
|
+
);
|
|
872
|
+
expect(result.passed).toBe(true);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
test('count multi-component: active + in_stock', async () => {
|
|
876
|
+
const result = await benchmark.runWithOutput(
|
|
877
|
+
'COUNT: active + in_stock',
|
|
878
|
+
async () => [await new Query()
|
|
879
|
+
.with(Product, {
|
|
880
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
881
|
+
})
|
|
882
|
+
.with(Inventory, {
|
|
883
|
+
filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
|
|
884
|
+
})
|
|
885
|
+
.count()],
|
|
886
|
+
{ targetP95: 200, iterations: 10 }
|
|
887
|
+
);
|
|
888
|
+
expect(result.passed).toBe(true);
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// ============================================================
|
|
893
|
+
// POPULATE (EAGER LOADING) TESTS
|
|
894
|
+
// ============================================================
|
|
895
|
+
|
|
896
|
+
describe('Populate / Eager Loading', () => {
|
|
897
|
+
test('populate single component', async () => {
|
|
898
|
+
const result = await benchmark.runWithOutput(
|
|
899
|
+
'Populate: Product',
|
|
900
|
+
() => new Query()
|
|
901
|
+
.with(Product)
|
|
902
|
+
.populate()
|
|
903
|
+
.take(50)
|
|
904
|
+
.exec(),
|
|
905
|
+
{ targetP95: 150, iterations: 10 }
|
|
906
|
+
);
|
|
907
|
+
expect(result.rowsReturned).toBe(50);
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
test('populate multi-component', async () => {
|
|
911
|
+
const result = await benchmark.runWithOutput(
|
|
912
|
+
'Populate: Product + Pricing',
|
|
913
|
+
() => new Query()
|
|
914
|
+
.with(Product)
|
|
915
|
+
.with(Pricing)
|
|
916
|
+
.populate()
|
|
917
|
+
.take(50)
|
|
918
|
+
.exec(),
|
|
919
|
+
{ targetP95: 200, iterations: 10 }
|
|
920
|
+
);
|
|
921
|
+
expect(result.rowsReturned).toBe(50);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
test('filtered populate', async () => {
|
|
925
|
+
const result = await benchmark.runWithOutput(
|
|
926
|
+
'Filtered Populate: active + in_stock',
|
|
927
|
+
() => new Query()
|
|
928
|
+
.with(Product, {
|
|
929
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
930
|
+
})
|
|
931
|
+
.with(Inventory, {
|
|
932
|
+
filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
|
|
933
|
+
})
|
|
934
|
+
.populate()
|
|
935
|
+
.take(50)
|
|
936
|
+
.exec(),
|
|
937
|
+
{ targetP95: 250, iterations: 10 }
|
|
938
|
+
);
|
|
939
|
+
expect(result.passed).toBe(true);
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// ============================================================
|
|
944
|
+
// REAL-WORLD SCENARIO TESTS
|
|
945
|
+
// ============================================================
|
|
946
|
+
|
|
947
|
+
describe('Real-World Scenarios', () => {
|
|
948
|
+
test('homepage featured products: active + in_stock + high rating + sorted', async () => {
|
|
949
|
+
const result = await benchmark.runWithOutput(
|
|
950
|
+
'Homepage: featured products',
|
|
951
|
+
() => new Query()
|
|
952
|
+
.with(Product, {
|
|
953
|
+
filters: [
|
|
954
|
+
{ field: 'status', operator: FilterOp.EQ, value: 'active' },
|
|
955
|
+
{ field: 'rating', operator: FilterOp.GTE, value: 4 }
|
|
956
|
+
]
|
|
957
|
+
})
|
|
958
|
+
.with(Inventory, {
|
|
959
|
+
filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
|
|
960
|
+
})
|
|
961
|
+
.sortBy(Product, 'rating', 'DESC')
|
|
962
|
+
.take(20)
|
|
963
|
+
.exec(),
|
|
964
|
+
{ targetP95: 250, iterations: 10 }
|
|
965
|
+
);
|
|
966
|
+
expect(result.passed).toBe(true);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
test('category page: Electronics with filters and pagination', async () => {
|
|
970
|
+
const result = await benchmark.runWithOutput(
|
|
971
|
+
'Category: Electronics page 2',
|
|
972
|
+
() => new Query()
|
|
973
|
+
.with(Product, {
|
|
974
|
+
filters: [
|
|
975
|
+
{ field: 'category', operator: FilterOp.EQ, value: 'Electronics' },
|
|
976
|
+
{ field: 'status', operator: FilterOp.EQ, value: 'active' }
|
|
977
|
+
]
|
|
978
|
+
})
|
|
979
|
+
.with(Inventory, {
|
|
980
|
+
filters: [
|
|
981
|
+
{ field: 'stockStatus', operator: FilterOp.IN, value: ['in_stock', 'low_stock'] }
|
|
982
|
+
]
|
|
983
|
+
})
|
|
984
|
+
.with(Pricing)
|
|
985
|
+
.sortBy(Pricing, 'basePrice', 'ASC')
|
|
986
|
+
.take(24)
|
|
987
|
+
.offset(24)
|
|
988
|
+
.exec(),
|
|
989
|
+
{ targetP95: 300, iterations: 10 }
|
|
990
|
+
);
|
|
991
|
+
expect(result.passed).toBe(true);
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
test('sale page: on-sale products sorted by discount', async () => {
|
|
995
|
+
const result = await benchmark.runWithOutput(
|
|
996
|
+
'Sale Page: sorted by discount',
|
|
997
|
+
() => new Query()
|
|
998
|
+
.with(Product, {
|
|
999
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
1000
|
+
})
|
|
1001
|
+
.with(Inventory, {
|
|
1002
|
+
filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
|
|
1003
|
+
})
|
|
1004
|
+
.with(Pricing, {
|
|
1005
|
+
filters: [{ field: 'isOnSale', operator: FilterOp.EQ, value: true }]
|
|
1006
|
+
})
|
|
1007
|
+
.sortBy(Pricing, 'discountPercent', 'DESC')
|
|
1008
|
+
.take(20)
|
|
1009
|
+
.exec(),
|
|
1010
|
+
{ targetP95: 300, iterations: 10 }
|
|
1011
|
+
);
|
|
1012
|
+
expect(result.passed).toBe(true);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
test('admin: low stock alert query', async () => {
|
|
1016
|
+
const result = await benchmark.runWithOutput(
|
|
1017
|
+
'Admin: low stock alert',
|
|
1018
|
+
() => new Query()
|
|
1019
|
+
.with(Product, {
|
|
1020
|
+
filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
|
|
1021
|
+
})
|
|
1022
|
+
.with(Inventory, {
|
|
1023
|
+
filters: [
|
|
1024
|
+
{ field: 'stockStatus', operator: FilterOp.IN, value: ['low_stock', 'out_of_stock'] }
|
|
1025
|
+
]
|
|
1026
|
+
})
|
|
1027
|
+
.with(Vendor)
|
|
1028
|
+
.populate()
|
|
1029
|
+
.take(100)
|
|
1030
|
+
.exec(),
|
|
1031
|
+
{ targetP95: 400, iterations: 10 }
|
|
1032
|
+
);
|
|
1033
|
+
expect(result.passed).toBe(true);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
test('vendor dashboard: products by vendor with metrics', async () => {
|
|
1037
|
+
const result = await benchmark.runWithOutput(
|
|
1038
|
+
'Vendor Dashboard: products + metrics',
|
|
1039
|
+
() => new Query()
|
|
1040
|
+
.with(Product)
|
|
1041
|
+
.with(Vendor, {
|
|
1042
|
+
filters: [{ field: 'vendorId', operator: FilterOp.EQ, value: 'VENDOR-001' }]
|
|
1043
|
+
})
|
|
1044
|
+
.with(Inventory)
|
|
1045
|
+
.with(ProductMetrics)
|
|
1046
|
+
.populate()
|
|
1047
|
+
.take(50)
|
|
1048
|
+
.exec(),
|
|
1049
|
+
{ targetP95: 400, iterations: 10 }
|
|
1050
|
+
);
|
|
1051
|
+
expect(result.passed).toBe(true);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
test('search results: price range + category + sorted', async () => {
|
|
1055
|
+
const result = await benchmark.runWithOutput(
|
|
1056
|
+
'Search: price 100-500 in Electronics',
|
|
1057
|
+
() => new Query()
|
|
1058
|
+
.with(Product, {
|
|
1059
|
+
filters: [
|
|
1060
|
+
{ field: 'category', operator: FilterOp.EQ, value: 'Electronics' },
|
|
1061
|
+
{ field: 'status', operator: FilterOp.EQ, value: 'active' }
|
|
1062
|
+
]
|
|
1063
|
+
})
|
|
1064
|
+
.with(Pricing, {
|
|
1065
|
+
filters: [
|
|
1066
|
+
{ field: 'basePrice', operator: FilterOp.GTE, value: 100 },
|
|
1067
|
+
{ field: 'basePrice', operator: FilterOp.LTE, value: 500 }
|
|
1068
|
+
]
|
|
1069
|
+
})
|
|
1070
|
+
.with(Inventory, {
|
|
1071
|
+
filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
|
|
1072
|
+
})
|
|
1073
|
+
.sortBy(Product, 'rating', 'DESC')
|
|
1074
|
+
.take(24)
|
|
1075
|
+
.exec(),
|
|
1076
|
+
{ targetP95: 350, iterations: 10 }
|
|
1077
|
+
);
|
|
1078
|
+
expect(result.passed).toBe(true);
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
});
|