bunsane 0.3.1 → 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 (224) hide show
  1. package/CHANGELOG.md +445 -318
  2. package/config/cache.config.ts +35 -1
  3. package/core/App.ts +24 -1064
  4. package/core/ArcheType.ts +78 -2110
  5. package/core/BatchLoader.ts +56 -32
  6. package/core/Entity.ts +85 -1043
  7. package/core/EntityHookManager.ts +52 -754
  8. package/core/Logger.ts +10 -0
  9. package/core/RequestContext.ts +64 -6
  10. package/core/RequestLoaders.ts +187 -36
  11. package/core/SchedulerManager.ts +28 -600
  12. package/core/app/bootstrap.ts +133 -0
  13. package/core/app/cors.ts +85 -0
  14. package/core/app/graphqlSetup.ts +56 -0
  15. package/core/app/healthEndpoints.ts +31 -0
  16. package/core/app/metricsCollector.ts +27 -0
  17. package/core/app/preparedStatementWarmup.ts +15 -0
  18. package/core/app/processHandlers.ts +43 -0
  19. package/core/app/requestRouter.ts +310 -0
  20. package/core/app/restRegistry.ts +80 -0
  21. package/core/app/shutdown.ts +97 -0
  22. package/core/app/studioRouter.ts +83 -0
  23. package/core/archetype/customTypes.ts +100 -0
  24. package/core/archetype/decorators.ts +171 -0
  25. package/core/archetype/fieldResolvers.ts +666 -0
  26. package/core/archetype/helpers.ts +29 -0
  27. package/core/archetype/relationLoader.ts +161 -0
  28. package/core/archetype/schemaBuilder.ts +141 -0
  29. package/core/archetype/weaver.ts +218 -0
  30. package/core/archetype/zodSchemaBuilder.ts +527 -0
  31. package/core/cache/CacheManager.ts +173 -267
  32. package/core/cache/CompressionUtils.ts +34 -3
  33. package/core/cache/MemoryCache.ts +40 -37
  34. package/core/cache/RedisCache.ts +4 -4
  35. package/core/cache/health.ts +30 -0
  36. package/core/cache/invalidation.ts +96 -0
  37. package/core/cache/strategies/writeInvalidate.ts +111 -0
  38. package/core/cache/strategies/writeThrough.ts +233 -0
  39. package/core/components/BaseComponent.ts +16 -8
  40. package/core/components/ComponentRegistry.ts +28 -0
  41. package/core/decorators/IndexedField.ts +1 -1
  42. package/core/entity/cacheStrategies.ts +97 -0
  43. package/core/entity/componentAccess.ts +364 -0
  44. package/core/entity/finders.ts +202 -0
  45. package/core/entity/pendingOps.ts +72 -0
  46. package/core/entity/saveEntity.ts +377 -0
  47. package/core/hooks/dispatcher.ts +439 -0
  48. package/core/hooks/guards.ts +155 -0
  49. package/core/hooks/registry.ts +247 -0
  50. package/core/metadata/definitions/Component.ts +1 -1
  51. package/core/metadata/index.ts +15 -4
  52. package/core/middleware/AccessLog.ts +8 -1
  53. package/core/middleware/RateLimit.ts +102 -105
  54. package/core/middleware/RequestId.ts +2 -9
  55. package/core/middleware/SecurityHeaders.ts +2 -11
  56. package/core/middleware/headers.ts +28 -0
  57. package/core/remote/OutboxWorker.ts +213 -183
  58. package/core/remote/RemoteManager.ts +401 -400
  59. package/core/remote/types.ts +153 -151
  60. package/core/requestScope.ts +34 -0
  61. package/core/scheduler/cronEvaluator.ts +174 -0
  62. package/core/scheduler/lifecycleHooks.ts +21 -0
  63. package/core/scheduler/lockCoordinator.ts +27 -0
  64. package/core/scheduler/metrics.ts +14 -0
  65. package/core/scheduler/taskRunner.ts +420 -0
  66. package/database/DatabaseHelper.ts +128 -101
  67. package/database/IndexingStrategy.ts +72 -2
  68. package/database/PreparedStatementCache.ts +20 -5
  69. package/database/cancellable.ts +35 -0
  70. package/database/index.ts +15 -3
  71. package/database/instrumentedDb.ts +141 -0
  72. package/endpoints/archetypes.ts +2 -8
  73. package/endpoints/tables.ts +6 -1
  74. package/gql/index.ts +1 -1
  75. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  76. package/package.json +22 -1
  77. package/query/CTENode.ts +5 -3
  78. package/query/ComponentInclusionNode.ts +240 -13
  79. package/query/OrNode.ts +6 -5
  80. package/query/Query.ts +203 -59
  81. package/query/QueryContext.ts +6 -0
  82. package/query/QueryDAG.ts +7 -2
  83. package/query/membershipSource.ts +66 -0
  84. package/storage/LocalStorageProvider.ts +8 -3
  85. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  86. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  87. package/studio/{index.html → dist/index.html} +3 -2
  88. package/swagger/generator.ts +11 -1
  89. package/upload/UploadManager.ts +8 -6
  90. package/utils/uuid.ts +40 -10
  91. package/.claude/settings.local.json +0 -47
  92. package/.prettierrc +0 -4
  93. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  94. package/.serena/memories/architecture.md +0 -154
  95. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  96. package/.serena/memories/code_style_and_conventions.md +0 -76
  97. package/.serena/memories/project_overview.md +0 -43
  98. package/.serena/memories/schema-dsl-plan.md +0 -107
  99. package/.serena/memories/suggested_commands.md +0 -80
  100. package/.serena/memories/typescript-compilation-status.md +0 -54
  101. package/.serena/project.yml +0 -114
  102. package/BunSane.jpg +0 -0
  103. package/CLAUDE.md +0 -198
  104. package/TODO.md +0 -2
  105. package/bun.lock +0 -302
  106. package/bunfig.toml +0 -10
  107. package/docs/SCALABILITY_PLAN.md +0 -175
  108. package/studio/bun.lock +0 -482
  109. package/studio/package.json +0 -39
  110. package/studio/postcss.config.js +0 -6
  111. package/studio/src/components/DataTable.tsx +0 -211
  112. package/studio/src/components/Layout.tsx +0 -13
  113. package/studio/src/components/PageContainer.tsx +0 -9
  114. package/studio/src/components/PageHeader.tsx +0 -13
  115. package/studio/src/components/SearchBar.tsx +0 -57
  116. package/studio/src/components/Sidebar.tsx +0 -294
  117. package/studio/src/components/ui/button.tsx +0 -56
  118. package/studio/src/components/ui/checkbox.tsx +0 -26
  119. package/studio/src/components/ui/input.tsx +0 -25
  120. package/studio/src/hooks/useDataTable.ts +0 -131
  121. package/studio/src/index.css +0 -36
  122. package/studio/src/lib/api.ts +0 -186
  123. package/studio/src/lib/utils.ts +0 -13
  124. package/studio/src/main.tsx +0 -17
  125. package/studio/src/pages/ArcheType.tsx +0 -239
  126. package/studio/src/pages/Components.tsx +0 -124
  127. package/studio/src/pages/EntityInspector.tsx +0 -302
  128. package/studio/src/pages/QueryRunner.tsx +0 -246
  129. package/studio/src/pages/Table.tsx +0 -94
  130. package/studio/src/pages/Welcome.tsx +0 -241
  131. package/studio/src/routes.tsx +0 -45
  132. package/studio/src/store/archeTypeSettings.ts +0 -30
  133. package/studio/src/store/studio.ts +0 -65
  134. package/studio/src/utils/columnHelpers.tsx +0 -114
  135. package/studio/studio-instructions.md +0 -81
  136. package/studio/tailwind.config.js +0 -77
  137. package/studio/utils.ts +0 -54
  138. package/studio/vite.config.js +0 -19
  139. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  140. package/tests/benchmark/bunfig.toml +0 -9
  141. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  142. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  143. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  144. package/tests/benchmark/fixtures/index.ts +0 -6
  145. package/tests/benchmark/index.ts +0 -22
  146. package/tests/benchmark/noop-preload.ts +0 -3
  147. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  148. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  149. package/tests/benchmark/runners/index.ts +0 -4
  150. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  151. package/tests/benchmark/scripts/generate-db.ts +0 -344
  152. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  153. package/tests/e2e/http.test.ts +0 -130
  154. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  155. package/tests/fixtures/components/TestOrder.ts +0 -23
  156. package/tests/fixtures/components/TestProduct.ts +0 -23
  157. package/tests/fixtures/components/TestUser.ts +0 -20
  158. package/tests/fixtures/components/index.ts +0 -6
  159. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  160. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  161. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  162. package/tests/helpers/MockRedisClient.ts +0 -113
  163. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  164. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  165. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  166. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  167. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  168. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  169. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  170. package/tests/integration/query/Query.exec.test.ts +0 -576
  171. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  172. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  173. package/tests/integration/remote/dlq.test.ts +0 -175
  174. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  175. package/tests/integration/remote/outbox.test.ts +0 -130
  176. package/tests/integration/remote/rpc.test.ts +0 -177
  177. package/tests/pglite-setup.ts +0 -62
  178. package/tests/setup.ts +0 -164
  179. package/tests/stress/BenchmarkRunner.ts +0 -203
  180. package/tests/stress/DataSeeder.ts +0 -190
  181. package/tests/stress/StressTestReporter.ts +0 -229
  182. package/tests/stress/cursor-perf-test.ts +0 -171
  183. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  184. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  185. package/tests/stress/index.ts +0 -7
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  187. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  188. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  189. package/tests/unit/BatchLoader.test.ts +0 -196
  190. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  191. package/tests/unit/cache/CacheManager.test.ts +0 -367
  192. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  193. package/tests/unit/cache/RedisCache.test.ts +0 -411
  194. package/tests/unit/entity/Entity.components.test.ts +0 -317
  195. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  196. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  197. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  198. package/tests/unit/entity/Entity.test.ts +0 -345
  199. package/tests/unit/gql/depthLimit.test.ts +0 -203
  200. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  201. package/tests/unit/health/Health.test.ts +0 -129
  202. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  203. package/tests/unit/middleware/Middleware.test.ts +0 -98
  204. package/tests/unit/middleware/RequestId.test.ts +0 -54
  205. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  206. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  207. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  208. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  209. package/tests/unit/query/Query.test.ts +0 -310
  210. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  211. package/tests/unit/remote/RemoteError.test.ts +0 -55
  212. package/tests/unit/remote/decorators.test.ts +0 -195
  213. package/tests/unit/remote/metrics.test.ts +0 -115
  214. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  215. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  216. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  217. package/tests/unit/schema/schema-integration.test.ts +0 -426
  218. package/tests/unit/schema/schema.test.ts +0 -580
  219. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  220. package/tests/unit/upload/RestUpload.test.ts +0 -267
  221. package/tests/unit/validateEnv.test.ts +0 -82
  222. package/tests/utils/entity-tracker.ts +0 -57
  223. package/tests/utils/index.ts +0 -13
  224. package/tests/utils/test-context.ts +0 -149
@@ -1,110 +0,0 @@
1
- /**
2
- * Integration tests for Entity.save timeout and cancellation behavior.
3
- *
4
- * Regression coverage for the production incident where Entity.save's wall-
5
- * clock timeout rejected the outer Promise but left the underlying Bun SQL
6
- * transaction mid-flight. Under pgbouncer transaction-mode pooling this
7
- * leaked backend PostgreSQL sessions into `idle in transaction` state,
8
- * exhausting the pool.
9
- *
10
- * These tests prove the invariants the fix must uphold:
11
- * 1. An aborted save leaves no partial rows — Bun SQL's transaction callback
12
- * throws, auto-ROLLBACK fires, backend connection is released.
13
- * 2. The connection pool stays healthy after repeated aborts — subsequent
14
- * saves on fresh entities still succeed.
15
- * 3. A save with no abort still commits normally.
16
- *
17
- * The wall-clock DB_QUERY_TIMEOUT path is module-cached at import time so it
18
- * is not exercised here directly. Manual verification on a real Postgres +
19
- * pgbouncer stack (with query_wait_timeout short enough to fire) should
20
- * confirm pg_stat_activity shows no `idle in transaction` backends after
21
- * this test suite runs. See the handoff doc (2026-04-18) for the repro steps.
22
- */
23
- import { describe, test, expect, beforeAll } from 'bun:test';
24
- import { Entity } from '../../../core/Entity';
25
- import db from '../../../database';
26
- import { TestUser } from '../../fixtures/components';
27
- import { createTestContext, ensureComponentsRegistered } from '../../utils';
28
-
29
- describe('Entity.save timeout and cancellation', () => {
30
- const ctx = createTestContext();
31
-
32
- beforeAll(async () => {
33
- await ensureComponentsRegistered(TestUser);
34
- });
35
-
36
- test('aborted doSave does not leave partial rows (transaction rolls back)', async () => {
37
- const entity = ctx.tracker.create();
38
- entity.add(TestUser, { name: 'aborted', email: 'a@example.com', age: 1 });
39
-
40
- const controller = new AbortController();
41
- // Abort immediately — the first in-flight query will be cancelled,
42
- // the transaction callback throws, Bun SQL issues ROLLBACK.
43
- queueMicrotask(() => controller.abort(new Error('simulated save timeout')));
44
-
45
- const result = db.transaction(async (trx) => {
46
- await entity.doSave(trx, controller.signal);
47
- });
48
-
49
- await expect(result).rejects.toBeDefined();
50
-
51
- // Entity must NOT exist — rollback invariant.
52
- const rows = await db`SELECT id FROM entities WHERE id = ${entity.id}`;
53
- expect(rows.length).toBe(0);
54
- });
55
-
56
- test('connection pool stays healthy after multiple aborted saves', async () => {
57
- // Repeatedly abort saves — if connections leaked, subsequent saves
58
- // would eventually block on pool acquire.
59
- for (let i = 0; i < 8; i++) {
60
- const entity = Entity.Create();
61
- entity.add(TestUser, { name: `aborted-${i}`, email: `a${i}@e.com`, age: i });
62
-
63
- const controller = new AbortController();
64
- queueMicrotask(() => controller.abort(new Error('simulated timeout')));
65
-
66
- await db.transaction(async (trx) => {
67
- await entity.doSave(trx, controller.signal);
68
- }).catch(() => { /* expected */ });
69
- }
70
-
71
- // A fresh save must still succeed on the pool that serviced the aborts.
72
- const healthy = ctx.tracker.create();
73
- healthy.add(TestUser, { name: 'healthy', email: 'h@e.com', age: 99 });
74
- await healthy.save();
75
-
76
- expect(healthy._persisted).toBe(true);
77
-
78
- const rows = await db`SELECT id FROM entities WHERE id = ${healthy.id}`;
79
- expect(rows.length).toBe(1);
80
- });
81
-
82
- test('doSave without signal behaves normally (backwards compatible)', async () => {
83
- const entity = ctx.tracker.create();
84
- entity.add(TestUser, { name: 'no-signal', email: 'n@e.com', age: 5 });
85
-
86
- await db.transaction(async (trx) => {
87
- await entity.doSave(trx); // no signal passed
88
- });
89
-
90
- const rows = await db`SELECT id FROM entities WHERE id = ${entity.id}`;
91
- expect(rows.length).toBe(1);
92
- });
93
-
94
- test('save() resolves even if post-commit cache work is slow (fire-and-forget)', async () => {
95
- // Cache handler is queued via queueMicrotask; save() must resolve as
96
- // soon as the DB transaction commits. We assert save resolves quickly
97
- // even though handleCacheAfterSave is awaited separately.
98
- const entity = ctx.tracker.create();
99
- entity.add(TestUser, { name: 'fast', email: 'f@e.com', age: 10 });
100
-
101
- const start = performance.now();
102
- await entity.save();
103
- const elapsed = performance.now() - start;
104
-
105
- expect(entity._persisted).toBe(true);
106
- // Generous bound — if cache were blocking save, timings under load
107
- // could stretch past the budget. This just guards gross regressions.
108
- expect(elapsed).toBeLessThan(5000);
109
- });
110
- });
@@ -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
- });