bunsane 0.3.2 → 0.5.0

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 (220) hide show
  1. package/CHANGELOG.md +471 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +93 -1020
  4. package/core/EntityHookManager.ts +52 -754
  5. package/core/Logger.ts +10 -0
  6. package/core/RequestContext.ts +94 -85
  7. package/core/RequestLoaders.ts +98 -5
  8. package/core/SchedulerManager.ts +28 -600
  9. package/core/app/cors.ts +2 -11
  10. package/core/app/preparedStatementWarmup.ts +9 -49
  11. package/core/app/requestRouter.ts +9 -8
  12. package/core/app/restRegistry.ts +8 -0
  13. package/core/archetype/fieldResolvers.ts +85 -40
  14. package/core/archetype/relationLoader.ts +135 -92
  15. package/core/cache/CacheManager.ts +91 -302
  16. package/core/cache/CompressionUtils.ts +34 -3
  17. package/core/cache/MemoryCache.ts +40 -37
  18. package/core/cache/RedisCache.ts +8 -7
  19. package/core/cache/health.ts +30 -0
  20. package/core/cache/invalidation.ts +96 -0
  21. package/core/cache/strategies/writeInvalidate.ts +111 -0
  22. package/core/cache/strategies/writeThrough.ts +233 -0
  23. package/core/components/BaseComponent.ts +25 -10
  24. package/core/components/ComponentRegistry.ts +28 -0
  25. package/core/decorators/IndexedField.ts +1 -1
  26. package/core/entity/cacheStrategies.ts +97 -0
  27. package/core/entity/componentAccess.ts +383 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/getCacheManager.ts +10 -0
  30. package/core/entity/pendingOps.ts +72 -0
  31. package/core/entity/saveEntity.ts +375 -0
  32. package/core/health.ts +93 -4
  33. package/core/hooks/dispatcher.ts +439 -0
  34. package/core/hooks/guards.ts +155 -0
  35. package/core/hooks/registry.ts +247 -0
  36. package/core/metadata/definitions/Component.ts +1 -1
  37. package/core/metadata/index.ts +15 -4
  38. package/core/middleware/RateLimit.ts +102 -105
  39. package/core/middleware/RequestId.ts +2 -9
  40. package/core/middleware/SecurityHeaders.ts +2 -11
  41. package/core/middleware/headers.ts +28 -0
  42. package/core/remote/OutboxWorker.ts +213 -183
  43. package/core/remote/RemoteManager.ts +401 -400
  44. package/core/remote/StreamConsumer.ts +535 -535
  45. package/core/remote/types.ts +153 -151
  46. package/core/requestScope.ts +34 -0
  47. package/core/scheduler/cronEvaluator.ts +174 -0
  48. package/core/scheduler/lifecycleHooks.ts +21 -0
  49. package/core/scheduler/lockCoordinator.ts +27 -0
  50. package/core/scheduler/metrics.ts +14 -0
  51. package/core/scheduler/taskRunner.ts +420 -0
  52. package/core/validateEnv.ts +10 -0
  53. package/database/DatabaseHelper.ts +128 -101
  54. package/database/IndexingStrategy.ts +72 -2
  55. package/database/PreparedStatementCache.ts +8 -2
  56. package/database/cancellable.ts +35 -22
  57. package/database/index.ts +29 -3
  58. package/database/instrumentedDb.ts +141 -141
  59. package/database/sqlHelpers.ts +3 -1
  60. package/endpoints/archetypes.ts +2 -8
  61. package/endpoints/tables.ts +6 -1
  62. package/gql/index.ts +1 -1
  63. package/gql/schema/index.ts +15 -4
  64. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  65. package/package.json +22 -1
  66. package/query/CTENode.ts +5 -3
  67. package/query/ComponentInclusionNode.ts +245 -14
  68. package/query/OrNode.ts +8 -19
  69. package/query/Query.ts +208 -79
  70. package/query/QueryContext.ts +6 -0
  71. package/query/QueryDAG.ts +7 -2
  72. package/query/membershipSource.ts +66 -0
  73. package/storage/LocalStorageProvider.ts +8 -3
  74. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  75. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  76. package/studio/{index.html → dist/index.html} +3 -2
  77. package/swagger/generator.ts +11 -1
  78. package/upload/UploadManager.ts +8 -6
  79. package/utils/uuid.ts +40 -10
  80. package/.claude/scheduled_tasks.lock +0 -1
  81. package/.claude/settings.local.json +0 -47
  82. package/.prettierrc +0 -4
  83. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  84. package/.serena/memories/architecture.md +0 -154
  85. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  86. package/.serena/memories/code_style_and_conventions.md +0 -76
  87. package/.serena/memories/project_overview.md +0 -43
  88. package/.serena/memories/schema-dsl-plan.md +0 -107
  89. package/.serena/memories/suggested_commands.md +0 -80
  90. package/.serena/memories/typescript-compilation-status.md +0 -54
  91. package/.serena/project.yml +0 -114
  92. package/BunSane.jpg +0 -0
  93. package/CLAUDE.md +0 -198
  94. package/TODO.md +0 -2
  95. package/bun.lock +0 -302
  96. package/bunfig.toml +0 -10
  97. package/docs/RFC_APP_REFACTOR.md +0 -248
  98. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  99. package/docs/SCALABILITY_PLAN.md +0 -175
  100. package/studio/bun.lock +0 -482
  101. package/studio/package.json +0 -39
  102. package/studio/postcss.config.js +0 -6
  103. package/studio/src/components/DataTable.tsx +0 -211
  104. package/studio/src/components/Layout.tsx +0 -13
  105. package/studio/src/components/PageContainer.tsx +0 -9
  106. package/studio/src/components/PageHeader.tsx +0 -13
  107. package/studio/src/components/SearchBar.tsx +0 -57
  108. package/studio/src/components/Sidebar.tsx +0 -294
  109. package/studio/src/components/ui/button.tsx +0 -56
  110. package/studio/src/components/ui/checkbox.tsx +0 -26
  111. package/studio/src/components/ui/input.tsx +0 -25
  112. package/studio/src/hooks/useDataTable.ts +0 -131
  113. package/studio/src/index.css +0 -36
  114. package/studio/src/lib/api.ts +0 -186
  115. package/studio/src/lib/utils.ts +0 -13
  116. package/studio/src/main.tsx +0 -17
  117. package/studio/src/pages/ArcheType.tsx +0 -239
  118. package/studio/src/pages/Components.tsx +0 -124
  119. package/studio/src/pages/EntityInspector.tsx +0 -302
  120. package/studio/src/pages/QueryRunner.tsx +0 -246
  121. package/studio/src/pages/Table.tsx +0 -94
  122. package/studio/src/pages/Welcome.tsx +0 -241
  123. package/studio/src/routes.tsx +0 -45
  124. package/studio/src/store/archeTypeSettings.ts +0 -30
  125. package/studio/src/store/studio.ts +0 -65
  126. package/studio/src/utils/columnHelpers.tsx +0 -114
  127. package/studio/studio-instructions.md +0 -81
  128. package/studio/tailwind.config.js +0 -77
  129. package/studio/utils.ts +0 -54
  130. package/studio/vite.config.js +0 -19
  131. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  132. package/tests/benchmark/bunfig.toml +0 -9
  133. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  134. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  135. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  136. package/tests/benchmark/fixtures/index.ts +0 -6
  137. package/tests/benchmark/index.ts +0 -22
  138. package/tests/benchmark/noop-preload.ts +0 -3
  139. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  140. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  141. package/tests/benchmark/runners/index.ts +0 -4
  142. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  143. package/tests/benchmark/scripts/generate-db.ts +0 -344
  144. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  145. package/tests/e2e/http.test.ts +0 -130
  146. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  147. package/tests/fixtures/components/TestOrder.ts +0 -23
  148. package/tests/fixtures/components/TestProduct.ts +0 -23
  149. package/tests/fixtures/components/TestUser.ts +0 -20
  150. package/tests/fixtures/components/index.ts +0 -6
  151. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  152. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  153. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  154. package/tests/helpers/MockRedisClient.ts +0 -113
  155. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  156. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  157. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  158. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  159. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  160. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  161. package/tests/integration/query/Query.abort.test.ts +0 -66
  162. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  163. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  164. package/tests/integration/query/Query.exec.test.ts +0 -576
  165. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  166. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  167. package/tests/integration/remote/dlq.test.ts +0 -175
  168. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  169. package/tests/integration/remote/outbox.test.ts +0 -130
  170. package/tests/integration/remote/rpc.test.ts +0 -177
  171. package/tests/pglite-setup.ts +0 -62
  172. package/tests/setup.ts +0 -164
  173. package/tests/stress/BenchmarkRunner.ts +0 -203
  174. package/tests/stress/DataSeeder.ts +0 -190
  175. package/tests/stress/StressTestReporter.ts +0 -229
  176. package/tests/stress/cursor-perf-test.ts +0 -171
  177. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  178. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  179. package/tests/stress/index.ts +0 -7
  180. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  181. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  182. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  183. package/tests/unit/BatchLoader.test.ts +0 -196
  184. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  185. package/tests/unit/cache/CacheManager.test.ts +0 -498
  186. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  187. package/tests/unit/cache/RedisCache.test.ts +0 -411
  188. package/tests/unit/database/cancellable.test.ts +0 -81
  189. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  190. package/tests/unit/entity/Entity.components.test.ts +0 -317
  191. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  192. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  193. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  194. package/tests/unit/entity/Entity.test.ts +0 -345
  195. package/tests/unit/gql/depthLimit.test.ts +0 -203
  196. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  197. package/tests/unit/health/Health.test.ts +0 -129
  198. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  199. package/tests/unit/middleware/Middleware.test.ts +0 -98
  200. package/tests/unit/middleware/RequestId.test.ts +0 -54
  201. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  202. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  203. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  204. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  205. package/tests/unit/query/Query.test.ts +0 -310
  206. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  207. package/tests/unit/remote/RemoteError.test.ts +0 -55
  208. package/tests/unit/remote/decorators.test.ts +0 -195
  209. package/tests/unit/remote/metrics.test.ts +0 -115
  210. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  211. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  212. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  213. package/tests/unit/schema/schema-integration.test.ts +0 -426
  214. package/tests/unit/schema/schema.test.ts +0 -580
  215. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  216. package/tests/unit/upload/RestUpload.test.ts +0 -267
  217. package/tests/unit/validateEnv.test.ts +0 -82
  218. package/tests/utils/entity-tracker.ts +0 -57
  219. package/tests/utils/index.ts +0 -13
  220. package/tests/utils/test-context.ts +0 -149
@@ -1,557 +0,0 @@
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
- });