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.
- package/CHANGELOG.md +445 -318
- package/config/cache.config.ts +35 -1
- package/core/App.ts +24 -1064
- package/core/ArcheType.ts +78 -2110
- package/core/BatchLoader.ts +56 -32
- package/core/Entity.ts +85 -1043
- package/core/EntityHookManager.ts +52 -754
- package/core/Logger.ts +10 -0
- package/core/RequestContext.ts +64 -6
- package/core/RequestLoaders.ts +187 -36
- package/core/SchedulerManager.ts +28 -600
- package/core/app/bootstrap.ts +133 -0
- package/core/app/cors.ts +85 -0
- package/core/app/graphqlSetup.ts +56 -0
- package/core/app/healthEndpoints.ts +31 -0
- package/core/app/metricsCollector.ts +27 -0
- package/core/app/preparedStatementWarmup.ts +15 -0
- package/core/app/processHandlers.ts +43 -0
- package/core/app/requestRouter.ts +310 -0
- package/core/app/restRegistry.ts +80 -0
- package/core/app/shutdown.ts +97 -0
- package/core/app/studioRouter.ts +83 -0
- package/core/archetype/customTypes.ts +100 -0
- package/core/archetype/decorators.ts +171 -0
- package/core/archetype/fieldResolvers.ts +666 -0
- package/core/archetype/helpers.ts +29 -0
- package/core/archetype/relationLoader.ts +161 -0
- package/core/archetype/schemaBuilder.ts +141 -0
- package/core/archetype/weaver.ts +218 -0
- package/core/archetype/zodSchemaBuilder.ts +527 -0
- package/core/cache/CacheManager.ts +173 -267
- package/core/cache/CompressionUtils.ts +34 -3
- package/core/cache/MemoryCache.ts +40 -37
- package/core/cache/RedisCache.ts +4 -4
- package/core/cache/health.ts +30 -0
- package/core/cache/invalidation.ts +96 -0
- package/core/cache/strategies/writeInvalidate.ts +111 -0
- package/core/cache/strategies/writeThrough.ts +233 -0
- package/core/components/BaseComponent.ts +16 -8
- package/core/components/ComponentRegistry.ts +28 -0
- package/core/decorators/IndexedField.ts +1 -1
- package/core/entity/cacheStrategies.ts +97 -0
- package/core/entity/componentAccess.ts +364 -0
- package/core/entity/finders.ts +202 -0
- package/core/entity/pendingOps.ts +72 -0
- package/core/entity/saveEntity.ts +377 -0
- package/core/hooks/dispatcher.ts +439 -0
- package/core/hooks/guards.ts +155 -0
- package/core/hooks/registry.ts +247 -0
- package/core/metadata/definitions/Component.ts +1 -1
- package/core/metadata/index.ts +15 -4
- package/core/middleware/AccessLog.ts +8 -1
- package/core/middleware/RateLimit.ts +102 -105
- package/core/middleware/RequestId.ts +2 -9
- package/core/middleware/SecurityHeaders.ts +2 -11
- package/core/middleware/headers.ts +28 -0
- package/core/remote/OutboxWorker.ts +213 -183
- package/core/remote/RemoteManager.ts +401 -400
- package/core/remote/types.ts +153 -151
- package/core/requestScope.ts +34 -0
- package/core/scheduler/cronEvaluator.ts +174 -0
- package/core/scheduler/lifecycleHooks.ts +21 -0
- package/core/scheduler/lockCoordinator.ts +27 -0
- package/core/scheduler/metrics.ts +14 -0
- package/core/scheduler/taskRunner.ts +420 -0
- package/database/DatabaseHelper.ts +128 -101
- package/database/IndexingStrategy.ts +72 -2
- package/database/PreparedStatementCache.ts +20 -5
- package/database/cancellable.ts +35 -0
- package/database/index.ts +15 -3
- package/database/instrumentedDb.ts +141 -0
- package/endpoints/archetypes.ts +2 -8
- package/endpoints/tables.ts +6 -1
- package/gql/index.ts +1 -1
- package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
- package/package.json +22 -1
- package/query/CTENode.ts +5 -3
- package/query/ComponentInclusionNode.ts +240 -13
- package/query/OrNode.ts +6 -5
- package/query/Query.ts +203 -59
- package/query/QueryContext.ts +6 -0
- package/query/QueryDAG.ts +7 -2
- package/query/membershipSource.ts +66 -0
- package/storage/LocalStorageProvider.ts +8 -3
- package/studio/dist/assets/index-BMZ67Npg.js +254 -0
- package/studio/dist/assets/index-BpbuYz9g.css +1 -0
- package/studio/{index.html → dist/index.html} +3 -2
- package/swagger/generator.ts +11 -1
- package/upload/UploadManager.ts +8 -6
- package/utils/uuid.ts +40 -10
- package/.claude/settings.local.json +0 -47
- package/.prettierrc +0 -4
- package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
- package/.serena/memories/architecture.md +0 -154
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
- package/.serena/memories/code_style_and_conventions.md +0 -76
- package/.serena/memories/project_overview.md +0 -43
- package/.serena/memories/schema-dsl-plan.md +0 -107
- package/.serena/memories/suggested_commands.md +0 -80
- package/.serena/memories/typescript-compilation-status.md +0 -54
- package/.serena/project.yml +0 -114
- package/BunSane.jpg +0 -0
- package/CLAUDE.md +0 -198
- package/TODO.md +0 -2
- package/bun.lock +0 -302
- package/bunfig.toml +0 -10
- package/docs/SCALABILITY_PLAN.md +0 -175
- package/studio/bun.lock +0 -482
- package/studio/package.json +0 -39
- package/studio/postcss.config.js +0 -6
- package/studio/src/components/DataTable.tsx +0 -211
- package/studio/src/components/Layout.tsx +0 -13
- package/studio/src/components/PageContainer.tsx +0 -9
- package/studio/src/components/PageHeader.tsx +0 -13
- package/studio/src/components/SearchBar.tsx +0 -57
- package/studio/src/components/Sidebar.tsx +0 -294
- package/studio/src/components/ui/button.tsx +0 -56
- package/studio/src/components/ui/checkbox.tsx +0 -26
- package/studio/src/components/ui/input.tsx +0 -25
- package/studio/src/hooks/useDataTable.ts +0 -131
- package/studio/src/index.css +0 -36
- package/studio/src/lib/api.ts +0 -186
- package/studio/src/lib/utils.ts +0 -13
- package/studio/src/main.tsx +0 -17
- package/studio/src/pages/ArcheType.tsx +0 -239
- package/studio/src/pages/Components.tsx +0 -124
- package/studio/src/pages/EntityInspector.tsx +0 -302
- package/studio/src/pages/QueryRunner.tsx +0 -246
- package/studio/src/pages/Table.tsx +0 -94
- package/studio/src/pages/Welcome.tsx +0 -241
- package/studio/src/routes.tsx +0 -45
- package/studio/src/store/archeTypeSettings.ts +0 -30
- package/studio/src/store/studio.ts +0 -65
- package/studio/src/utils/columnHelpers.tsx +0 -114
- package/studio/studio-instructions.md +0 -81
- package/studio/tailwind.config.js +0 -77
- package/studio/utils.ts +0 -54
- package/studio/vite.config.js +0 -19
- package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
- package/tests/benchmark/bunfig.toml +0 -9
- package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
- package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
- package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
- package/tests/benchmark/fixtures/index.ts +0 -6
- package/tests/benchmark/index.ts +0 -22
- package/tests/benchmark/noop-preload.ts +0 -3
- package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
- package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
- package/tests/benchmark/runners/index.ts +0 -4
- package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
- package/tests/benchmark/scripts/generate-db.ts +0 -344
- package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
- package/tests/e2e/http.test.ts +0 -130
- package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
- package/tests/fixtures/components/TestOrder.ts +0 -23
- package/tests/fixtures/components/TestProduct.ts +0 -23
- package/tests/fixtures/components/TestUser.ts +0 -20
- package/tests/fixtures/components/index.ts +0 -6
- package/tests/graphql/SchemaGeneration.test.ts +0 -90
- package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
- package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
- package/tests/helpers/MockRedisClient.ts +0 -113
- package/tests/helpers/MockRedisStreamServer.ts +0 -448
- package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
- package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
- package/tests/integration/entity/Entity.persistence.test.ts +0 -333
- package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
- package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
- package/tests/integration/query/Query.edgeCases.test.ts +0 -595
- package/tests/integration/query/Query.exec.test.ts +0 -576
- package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
- package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
- package/tests/integration/remote/dlq.test.ts +0 -175
- package/tests/integration/remote/event-dispatch.test.ts +0 -114
- package/tests/integration/remote/outbox.test.ts +0 -130
- package/tests/integration/remote/rpc.test.ts +0 -177
- package/tests/pglite-setup.ts +0 -62
- package/tests/setup.ts +0 -164
- package/tests/stress/BenchmarkRunner.ts +0 -203
- package/tests/stress/DataSeeder.ts +0 -190
- package/tests/stress/StressTestReporter.ts +0 -229
- package/tests/stress/cursor-perf-test.ts +0 -171
- package/tests/stress/fixtures/RealisticComponents.ts +0 -235
- package/tests/stress/fixtures/StressTestComponents.ts +0 -58
- package/tests/stress/index.ts +0 -7
- package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
- package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
- package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
- package/tests/unit/BatchLoader.test.ts +0 -196
- package/tests/unit/archetype/ArcheType.test.ts +0 -107
- package/tests/unit/cache/CacheManager.test.ts +0 -367
- package/tests/unit/cache/MemoryCache.test.ts +0 -260
- package/tests/unit/cache/RedisCache.test.ts +0 -411
- package/tests/unit/entity/Entity.components.test.ts +0 -317
- package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
- package/tests/unit/entity/Entity.reload.test.ts +0 -63
- package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
- package/tests/unit/entity/Entity.test.ts +0 -345
- package/tests/unit/gql/depthLimit.test.ts +0 -203
- package/tests/unit/gql/operationMiddleware.test.ts +0 -293
- package/tests/unit/health/Health.test.ts +0 -129
- package/tests/unit/middleware/AccessLog.test.ts +0 -37
- package/tests/unit/middleware/Middleware.test.ts +0 -98
- package/tests/unit/middleware/RequestId.test.ts +0 -54
- package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
- package/tests/unit/query/FilterBuilder.test.ts +0 -111
- package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
- package/tests/unit/query/Query.emptyString.test.ts +0 -69
- package/tests/unit/query/Query.test.ts +0 -310
- package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
- package/tests/unit/remote/RemoteError.test.ts +0 -55
- package/tests/unit/remote/decorators.test.ts +0 -195
- package/tests/unit/remote/metrics.test.ts +0 -115
- package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
- package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
- package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
- package/tests/unit/schema/schema-integration.test.ts +0 -426
- package/tests/unit/schema/schema.test.ts +0 -580
- package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
- package/tests/unit/upload/RestUpload.test.ts +0 -267
- package/tests/unit/validateEnv.test.ts +0 -82
- package/tests/utils/entity-tracker.ts +0 -57
- package/tests/utils/index.ts +0 -13
- 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
|
-
});
|