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