bunsane 0.1.4 → 0.2.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/.claude/settings.local.json +47 -0
- package/.claude/skills/update-memory.md +74 -0
- package/.prettierrc +4 -0
- package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
- package/.serena/memories/architecture.md +154 -0
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
- package/.serena/memories/code_style_and_conventions.md +76 -0
- package/.serena/memories/project_overview.md +43 -0
- package/.serena/memories/schema-dsl-plan.md +107 -0
- package/.serena/memories/suggested_commands.md +80 -0
- package/.serena/memories/typescript-compilation-status.md +54 -0
- package/.serena/project.yml +114 -0
- package/TODO.md +1 -7
- package/bun.lock +150 -4
- package/bunfig.toml +10 -0
- package/config/cache.config.ts +77 -0
- package/config/upload.config.ts +4 -5
- package/core/App.ts +870 -123
- package/core/ArcheType.ts +2268 -377
- package/core/BatchLoader.ts +181 -71
- package/core/Config.ts +153 -0
- package/core/Decorators.ts +4 -1
- package/core/Entity.ts +621 -92
- package/core/EntityHookManager.ts +1 -1
- package/core/EntityInterface.ts +3 -1
- package/core/EntityManager.ts +1 -13
- package/core/ErrorHandler.ts +8 -2
- package/core/Logger.ts +9 -0
- package/core/Middleware.ts +34 -0
- package/core/RequestContext.ts +5 -1
- package/core/RequestLoaders.ts +227 -93
- package/core/SchedulerManager.ts +193 -52
- package/core/cache/CacheAnalytics.ts +399 -0
- package/core/cache/CacheFactory.ts +145 -0
- package/core/cache/CacheManager.ts +520 -0
- package/core/cache/CacheProvider.ts +34 -0
- package/core/cache/CacheWarmer.ts +157 -0
- package/core/cache/CompressionUtils.ts +110 -0
- package/core/cache/MemoryCache.ts +251 -0
- package/core/cache/MultiLevelCache.ts +180 -0
- package/core/cache/NoOpCache.ts +53 -0
- package/core/cache/RedisCache.ts +464 -0
- package/core/cache/TTLStrategy.ts +254 -0
- package/core/cache/index.ts +6 -0
- package/core/components/BaseComponent.ts +120 -0
- package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
- package/core/components/Decorators.ts +88 -0
- package/core/components/Interfaces.ts +7 -0
- package/core/components/index.ts +5 -0
- package/core/decorators/EntityHooks.ts +0 -3
- package/core/decorators/IndexedField.ts +26 -0
- package/core/decorators/ScheduledTask.ts +0 -47
- package/core/events/EntityLifecycleEvents.ts +1 -1
- package/core/health.ts +112 -0
- package/core/metadata/definitions/ArcheType.ts +14 -0
- package/core/metadata/definitions/Component.ts +9 -0
- package/core/metadata/definitions/gqlObject.ts +1 -1
- package/core/metadata/index.ts +42 -1
- package/core/metadata/metadata-storage.ts +28 -2
- package/core/middleware/AccessLog.ts +59 -0
- package/core/middleware/RequestId.ts +38 -0
- package/core/middleware/SecurityHeaders.ts +62 -0
- package/core/middleware/index.ts +3 -0
- package/core/scheduler/DistributedLock.ts +266 -0
- package/core/scheduler/index.ts +15 -0
- package/core/validateEnv.ts +92 -0
- package/database/DatabaseHelper.ts +416 -40
- package/database/IndexingStrategy.ts +342 -0
- package/database/PreparedStatementCache.ts +226 -0
- package/database/index.ts +32 -7
- package/database/sqlHelpers.ts +14 -2
- package/endpoints/archetypes.ts +362 -0
- package/endpoints/components.ts +58 -0
- package/endpoints/entity.ts +80 -0
- package/endpoints/index.ts +27 -0
- package/endpoints/query.ts +93 -0
- package/endpoints/stats.ts +76 -0
- package/endpoints/tables.ts +212 -0
- package/endpoints/types.ts +155 -0
- package/gql/ArchetypeOperations.ts +32 -86
- package/gql/Generator.ts +27 -315
- package/gql/GeneratorV2.ts +37 -0
- package/gql/builders/InputTypeBuilder.ts +99 -0
- package/gql/builders/ResolverBuilder.ts +234 -0
- package/gql/builders/TypeDefBuilder.ts +105 -0
- package/gql/builders/index.ts +3 -0
- package/gql/decorators/Upload.ts +1 -1
- package/gql/depthLimit.ts +85 -0
- package/gql/graph/GraphNode.ts +224 -0
- package/gql/graph/SchemaGraph.ts +278 -0
- package/gql/helpers.ts +8 -2
- package/gql/index.ts +56 -4
- package/gql/middleware.ts +79 -0
- package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
- package/gql/orchestration/index.ts +1 -0
- package/gql/scanner/ServiceScanner.ts +347 -0
- package/gql/schema/index.ts +458 -0
- package/gql/strategies/TypeGenerationStrategy.ts +329 -0
- package/gql/types.ts +1 -0
- package/gql/utils/TypeSignature.ts +220 -0
- package/gql/utils/index.ts +1 -0
- package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
- package/gql/visitors/DeduplicationVisitor.ts +82 -0
- package/gql/visitors/GraphVisitor.ts +78 -0
- package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
- package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
- package/gql/visitors/TypeCollectorVisitor.ts +79 -0
- package/gql/visitors/VisitorComposer.ts +96 -0
- package/gql/visitors/index.ts +7 -0
- package/package.json +59 -37
- package/plugins/index.ts +2 -2
- package/query/CTENode.ts +97 -0
- package/query/ComponentInclusionNode.ts +689 -0
- package/query/FilterBuilder.ts +127 -0
- package/query/FilterBuilderRegistry.ts +202 -0
- package/query/OrNode.ts +517 -0
- package/query/OrQuery.ts +42 -0
- package/query/Query.ts +1022 -0
- package/query/QueryContext.ts +170 -0
- package/query/QueryDAG.ts +122 -0
- package/query/QueryNode.ts +65 -0
- package/query/SourceNode.ts +53 -0
- package/query/builders/FullTextSearchBuilder.ts +236 -0
- package/query/index.ts +21 -0
- package/scheduler/index.ts +40 -8
- package/service/Service.ts +2 -1
- package/service/ServiceRegistry.ts +6 -5
- package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
- package/storage/S3StorageProvider.ts +316 -0
- package/{core/storage → storage}/StorageProvider.ts +7 -3
- package/studio/bun.lock +482 -0
- package/studio/index.html +13 -0
- package/studio/package.json +39 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/components/DataTable.tsx +211 -0
- package/studio/src/components/Layout.tsx +13 -0
- package/studio/src/components/PageContainer.tsx +9 -0
- package/studio/src/components/PageHeader.tsx +13 -0
- package/studio/src/components/SearchBar.tsx +57 -0
- package/studio/src/components/Sidebar.tsx +294 -0
- package/studio/src/components/ui/button.tsx +56 -0
- package/studio/src/components/ui/checkbox.tsx +26 -0
- package/studio/src/components/ui/input.tsx +25 -0
- package/studio/src/hooks/useDataTable.ts +131 -0
- package/studio/src/index.css +36 -0
- package/studio/src/lib/api.ts +186 -0
- package/studio/src/lib/utils.ts +13 -0
- package/studio/src/main.tsx +17 -0
- package/studio/src/pages/ArcheType.tsx +239 -0
- package/studio/src/pages/Components.tsx +124 -0
- package/studio/src/pages/EntityInspector.tsx +302 -0
- package/studio/src/pages/QueryRunner.tsx +246 -0
- package/studio/src/pages/Table.tsx +94 -0
- package/studio/src/pages/Welcome.tsx +241 -0
- package/studio/src/routes.tsx +45 -0
- package/studio/src/store/archeTypeSettings.ts +30 -0
- package/studio/src/store/studio.ts +65 -0
- package/studio/src/utils/columnHelpers.tsx +114 -0
- package/studio/studio-instructions.md +81 -0
- package/studio/tailwind.config.js +77 -0
- package/studio/tsconfig.json +24 -0
- package/studio/utils.ts +54 -0
- package/studio/vite.config.js +19 -0
- package/swagger/generator.ts +1 -1
- package/tests/e2e/http.test.ts +126 -0
- package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
- package/tests/fixtures/components/TestOrder.ts +23 -0
- package/tests/fixtures/components/TestProduct.ts +23 -0
- package/tests/fixtures/components/TestUser.ts +20 -0
- package/tests/fixtures/components/index.ts +6 -0
- package/tests/graphql/SchemaGeneration.test.ts +90 -0
- package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
- package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
- package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
- package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
- package/tests/integration/entity/Entity.persistence.test.ts +333 -0
- package/tests/integration/query/Query.exec.test.ts +523 -0
- package/tests/pglite-setup.ts +61 -0
- package/tests/setup.ts +164 -0
- package/tests/stress/BenchmarkRunner.ts +203 -0
- package/tests/stress/DataSeeder.ts +190 -0
- package/tests/stress/StressTestReporter.ts +229 -0
- package/tests/stress/cursor-perf-test.ts +171 -0
- package/tests/stress/fixtures/StressTestComponents.ts +58 -0
- package/tests/stress/index.ts +7 -0
- package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
- package/tests/unit/BatchLoader.test.ts +82 -0
- package/tests/unit/archetype/ArcheType.test.ts +107 -0
- package/tests/unit/cache/CacheManager.test.ts +347 -0
- package/tests/unit/cache/MemoryCache.test.ts +260 -0
- package/tests/unit/cache/RedisCache.test.ts +411 -0
- package/tests/unit/entity/Entity.components.test.ts +244 -0
- package/tests/unit/entity/Entity.test.ts +345 -0
- package/tests/unit/gql/depthLimit.test.ts +203 -0
- package/tests/unit/gql/operationMiddleware.test.ts +293 -0
- package/tests/unit/health/Health.test.ts +129 -0
- package/tests/unit/middleware/AccessLog.test.ts +37 -0
- package/tests/unit/middleware/Middleware.test.ts +98 -0
- package/tests/unit/middleware/RequestId.test.ts +54 -0
- package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
- package/tests/unit/query/FilterBuilder.test.ts +111 -0
- package/tests/unit/query/Query.test.ts +308 -0
- package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
- package/tests/unit/schema/schema-integration.test.ts +426 -0
- package/tests/unit/schema/schema.test.ts +580 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
- package/tests/unit/upload/RestUpload.test.ts +267 -0
- package/tests/unit/validateEnv.test.ts +82 -0
- package/tests/utils/entity-tracker.ts +57 -0
- package/tests/utils/index.ts +13 -0
- package/tests/utils/test-context.ts +149 -0
- package/tsconfig.json +5 -1
- package/types/archetype.types.ts +6 -0
- package/types/hooks.types.ts +1 -1
- package/types/query.types.ts +110 -0
- package/types/scheduler.types.ts +68 -7
- package/types/upload.types.ts +1 -0
- package/{core → upload}/FileValidator.ts +10 -1
- package/upload/RestUpload.ts +130 -0
- package/{core/components → upload}/UploadComponent.ts +11 -11
- package/{core → upload}/UploadManager.ts +3 -3
- package/upload/index.ts +23 -7
- package/utils/UploadHelper.ts +27 -6
- package/utils/cronParser.ts +16 -6
- package/.github/workflows/deploy-docs.yml +0 -57
- package/core/Components.ts +0 -202
- package/core/EntityCache.ts +0 -15
- package/core/Query.ts +0 -880
- package/docs/README.md +0 -149
- package/docs/_coverpage.md +0 -36
- package/docs/_sidebar.md +0 -23
- package/docs/api/core.md +0 -568
- package/docs/api/hooks.md +0 -554
- package/docs/api/index.md +0 -222
- package/docs/api/query.md +0 -678
- package/docs/api/service.md +0 -744
- package/docs/core-concepts/archetypes.md +0 -512
- package/docs/core-concepts/components.md +0 -498
- package/docs/core-concepts/entity.md +0 -314
- package/docs/core-concepts/hooks.md +0 -683
- package/docs/core-concepts/query.md +0 -588
- package/docs/core-concepts/services.md +0 -647
- package/docs/examples/code-examples.md +0 -425
- package/docs/getting-started.md +0 -337
- package/docs/index.html +0 -97
- package/tests/bench/insert.bench.ts +0 -60
- package/tests/bench/relations.bench.ts +0 -270
- package/tests/bench/sorting.bench.ts +0 -416
- package/tests/component-hooks-simple.test.ts +0 -117
- package/tests/component-hooks.test.ts +0 -1461
- package/tests/component.test.ts +0 -339
- package/tests/errorHandling.test.ts +0 -155
- package/tests/hooks.test.ts +0 -667
- package/tests/query-sorting.test.ts +0 -101
- package/tests/query.test.ts +0 -81
- package/tests/relations.test.ts +0 -170
- package/tests/scheduler.test.ts +0 -724
package/query/OrNode.ts
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { QueryNode } from "./QueryNode";
|
|
2
|
+
import type { QueryResult } from "./QueryNode";
|
|
3
|
+
import { QueryContext } from "./QueryContext";
|
|
4
|
+
import { OrQuery } from "./OrQuery";
|
|
5
|
+
import { ComponentRegistry } from "../core/components";
|
|
6
|
+
import { shouldUseDirectPartition } from "../core/Config";
|
|
7
|
+
|
|
8
|
+
export class OrNode extends QueryNode {
|
|
9
|
+
private orQuery: OrQuery;
|
|
10
|
+
|
|
11
|
+
constructor(orQuery: OrQuery) {
|
|
12
|
+
super();
|
|
13
|
+
this.orQuery = orQuery;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private getComponentTableName(compId: string): string {
|
|
17
|
+
if (shouldUseDirectPartition()) {
|
|
18
|
+
return ComponentRegistry.getPartitionTableName(compId) || 'components';
|
|
19
|
+
}
|
|
20
|
+
return 'components';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if we can use the optimized UNION ALL approach with direct partition access.
|
|
25
|
+
* This works for both:
|
|
26
|
+
* - Multiple different component types (each queries its own partition)
|
|
27
|
+
* - Same component type with different filters (queries same partition, UNION dedupes results)
|
|
28
|
+
*/
|
|
29
|
+
private canUseUnionAllOptimization(): boolean {
|
|
30
|
+
if (!shouldUseDirectPartition()) return false;
|
|
31
|
+
|
|
32
|
+
// Verify all components are registered and have valid partition tables
|
|
33
|
+
for (const branch of this.orQuery.branches) {
|
|
34
|
+
const compId = ComponentRegistry.getComponentId(branch.component.name);
|
|
35
|
+
if (!compId) return false;
|
|
36
|
+
// Ensure partition table exists for this component
|
|
37
|
+
const partitionTable = ComponentRegistry.getPartitionTableName(compId);
|
|
38
|
+
if (!partitionTable) return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// With direct partition access, always use the optimized path
|
|
42
|
+
// The UNION automatically dedupes results when branches use the same partition
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Optimized UNION ALL execution for OR queries with direct partition access
|
|
48
|
+
* Each branch queries its partition directly using simple queries
|
|
49
|
+
* This avoids the complex EXISTS subqueries of the original implementation
|
|
50
|
+
*/
|
|
51
|
+
private executeUnionAllOptimized(context: QueryContext): QueryResult {
|
|
52
|
+
// Special case: if all branches use the same component type, combine into single query with OR conditions
|
|
53
|
+
const componentTypes = new Set<string>();
|
|
54
|
+
for (const branch of this.orQuery.branches) {
|
|
55
|
+
const compId = ComponentRegistry.getComponentId(branch.component.name);
|
|
56
|
+
if (compId) {
|
|
57
|
+
componentTypes.add(compId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (componentTypes.size === 1) {
|
|
62
|
+
return this.executeSingleComponentOptimized(context);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Original multi-component logic
|
|
66
|
+
const branches: string[] = [];
|
|
67
|
+
let paramIndex = context.paramIndex;
|
|
68
|
+
|
|
69
|
+
// Build SQL for each branch - direct, simple partition queries
|
|
70
|
+
for (const branch of this.orQuery.branches) {
|
|
71
|
+
const componentId = ComponentRegistry.getComponentId(branch.component.name);
|
|
72
|
+
if (!componentId) {
|
|
73
|
+
throw new Error(`Component ${branch.component.name} is not registered`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const partitionTable = ComponentRegistry.getPartitionTableName(componentId) || 'components';
|
|
77
|
+
|
|
78
|
+
// Simple, direct query to partition table - no EXISTS, no subqueries
|
|
79
|
+
let branchSql = `SELECT entity_id FROM ${partitionTable} WHERE type_id = $${paramIndex} AND deleted_at IS NULL`;
|
|
80
|
+
|
|
81
|
+
context.params.push(componentId);
|
|
82
|
+
paramIndex++;
|
|
83
|
+
|
|
84
|
+
// Add filters for this branch - inline in WHERE clause
|
|
85
|
+
if (branch.filters && branch.filters.length > 0) {
|
|
86
|
+
for (const filter of branch.filters) {
|
|
87
|
+
const { field, operator, value } = filter;
|
|
88
|
+
const jsonPath = `data->>'${field}'`;
|
|
89
|
+
|
|
90
|
+
switch (operator) {
|
|
91
|
+
case "=":
|
|
92
|
+
case ">":
|
|
93
|
+
case "<":
|
|
94
|
+
case ">=":
|
|
95
|
+
case "<=":
|
|
96
|
+
case "!=":
|
|
97
|
+
if (typeof value === "string") {
|
|
98
|
+
branchSql += ` AND ${jsonPath} ${operator} $${paramIndex}::text`;
|
|
99
|
+
} else {
|
|
100
|
+
branchSql += ` AND (${jsonPath})::numeric ${operator} $${paramIndex}`;
|
|
101
|
+
}
|
|
102
|
+
context.params.push(value);
|
|
103
|
+
paramIndex++;
|
|
104
|
+
break;
|
|
105
|
+
case "LIKE":
|
|
106
|
+
case "ILIKE":
|
|
107
|
+
branchSql += ` AND ${jsonPath} ${operator} $${paramIndex}::text`;
|
|
108
|
+
context.params.push(value);
|
|
109
|
+
paramIndex++;
|
|
110
|
+
break;
|
|
111
|
+
case "IN":
|
|
112
|
+
if (Array.isArray(value)) {
|
|
113
|
+
const placeholders = value.map(() => `$${paramIndex++}`).join(', ');
|
|
114
|
+
branchSql += ` AND ${jsonPath} IN (${placeholders})`;
|
|
115
|
+
context.params.push(...value);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
case "NOT IN":
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
const placeholders = value.map(() => `$${paramIndex++}`).join(', ');
|
|
121
|
+
branchSql += ` AND ${jsonPath} NOT IN (${placeholders})`;
|
|
122
|
+
context.params.push(...value);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
branches.push(branchSql);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Combine with UNION (automatically dedupes) - simpler than UNION ALL + DISTINCT wrapper
|
|
135
|
+
let sql = `SELECT entity_id as id FROM (${branches.join(' UNION ')}) AS or_results`;
|
|
136
|
+
|
|
137
|
+
// Apply global constraints
|
|
138
|
+
const conditions: string[] = [];
|
|
139
|
+
|
|
140
|
+
// Add entity exclusions
|
|
141
|
+
if (context.excludedEntityIds.size > 0) {
|
|
142
|
+
const excludedIds = Array.from(context.excludedEntityIds);
|
|
143
|
+
const placeholders = excludedIds.map(() => `$${paramIndex++}`).join(', ');
|
|
144
|
+
conditions.push(`entity_id NOT IN (${placeholders})`);
|
|
145
|
+
context.params.push(...excludedIds);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add component exclusions
|
|
149
|
+
if (context.excludedComponentIds.size > 0) {
|
|
150
|
+
const excludedTypes = Array.from(context.excludedComponentIds);
|
|
151
|
+
const placeholders = excludedTypes.map(() => `$${paramIndex++}`).join(', ');
|
|
152
|
+
conditions.push(`NOT EXISTS (SELECT 1 FROM entity_components ec_ex WHERE ec_ex.entity_id = or_results.id AND ec_ex.type_id IN (${placeholders}) AND ec_ex.deleted_at IS NULL)`);
|
|
153
|
+
context.params.push(...excludedTypes);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (conditions.length > 0) {
|
|
157
|
+
sql += ` WHERE ${conditions.join(' AND ')}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Add ordering
|
|
161
|
+
sql += " ORDER BY id";
|
|
162
|
+
|
|
163
|
+
// Add pagination
|
|
164
|
+
if (context.limit !== null) {
|
|
165
|
+
sql += ` LIMIT $${paramIndex++}`;
|
|
166
|
+
context.params.push(context.limit);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (context.offsetValue > 0) {
|
|
170
|
+
sql += ` OFFSET $${paramIndex++}`;
|
|
171
|
+
context.params.push(context.offsetValue);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
context.paramIndex = paramIndex;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
sql,
|
|
178
|
+
params: context.params,
|
|
179
|
+
context
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Special optimized execution for OR queries where all branches use the same component type
|
|
185
|
+
* Uses OR conditions in a single query instead of UNION to avoid PostgreSQL parameter type inference issues
|
|
186
|
+
*/
|
|
187
|
+
private executeSingleComponentOptimized(context: QueryContext): QueryResult {
|
|
188
|
+
let paramIndex = context.paramIndex;
|
|
189
|
+
|
|
190
|
+
// Get the single component info
|
|
191
|
+
const branch = this.orQuery.branches[0];
|
|
192
|
+
if(!branch) {
|
|
193
|
+
throw new Error("OrNode: No branches found in OrQuery");
|
|
194
|
+
}
|
|
195
|
+
const componentId = ComponentRegistry.getComponentId(branch.component.name);
|
|
196
|
+
if (!componentId) {
|
|
197
|
+
throw new Error(`Component ${branch.component.name} is not registered`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const partitionTable = ComponentRegistry.getPartitionTableName(componentId) || 'components';
|
|
201
|
+
|
|
202
|
+
// Build WHERE conditions for all branches
|
|
203
|
+
const orConditions: string[] = [];
|
|
204
|
+
|
|
205
|
+
for (const branch of this.orQuery.branches) {
|
|
206
|
+
const conditions: string[] = [];
|
|
207
|
+
|
|
208
|
+
// Use literal component type value (no parameter) to avoid type inference issues
|
|
209
|
+
conditions.push(`type_id = '${componentId}'`);
|
|
210
|
+
|
|
211
|
+
// Add filters for this branch
|
|
212
|
+
if (branch.filters && branch.filters.length > 0) {
|
|
213
|
+
for (const filter of branch.filters) {
|
|
214
|
+
const { field, operator, value } = filter;
|
|
215
|
+
const jsonPath = `data->>'${field}'`;
|
|
216
|
+
|
|
217
|
+
switch (operator) {
|
|
218
|
+
case "=":
|
|
219
|
+
case ">":
|
|
220
|
+
case "<":
|
|
221
|
+
case ">=":
|
|
222
|
+
case "<=":
|
|
223
|
+
case "!=":
|
|
224
|
+
case "LIKE":
|
|
225
|
+
case "ILIKE":
|
|
226
|
+
// Note: data->>'field' returns text, so no cast needed
|
|
227
|
+
// Explicit casting can cause issues with Bun's SQL parameter type inference
|
|
228
|
+
conditions.push(`${jsonPath} ${operator} $${paramIndex}`);
|
|
229
|
+
context.params.push(value);
|
|
230
|
+
paramIndex++;
|
|
231
|
+
break;
|
|
232
|
+
case "IN":
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
const placeholders = value.map(() => `$${paramIndex++}`).join(', ');
|
|
235
|
+
conditions.push(`${jsonPath} IN (${placeholders})`);
|
|
236
|
+
context.params.push(...value);
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
case "NOT IN":
|
|
240
|
+
if (Array.isArray(value)) {
|
|
241
|
+
const placeholders = value.map(() => `$${paramIndex++}`).join(', ');
|
|
242
|
+
conditions.push(`${jsonPath} NOT IN (${placeholders})`);
|
|
243
|
+
context.params.push(...value);
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
default:
|
|
247
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Combine conditions for this branch with AND
|
|
253
|
+
orConditions.push(`(${conditions.join(' AND ')})`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Build the main query
|
|
257
|
+
let sql = `SELECT entity_id as id FROM ${partitionTable} WHERE deleted_at IS NULL AND (${orConditions.join(' OR ')})`;
|
|
258
|
+
|
|
259
|
+
// Apply global constraints
|
|
260
|
+
const conditions: string[] = [];
|
|
261
|
+
|
|
262
|
+
// Add entity exclusions
|
|
263
|
+
if (context.excludedEntityIds.size > 0) {
|
|
264
|
+
const excludedIds = Array.from(context.excludedEntityIds);
|
|
265
|
+
const placeholders = excludedIds.map(() => `$${paramIndex++}`).join(', ');
|
|
266
|
+
conditions.push(`entity_id NOT IN (${placeholders})`);
|
|
267
|
+
context.params.push(...excludedIds);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Add component exclusions
|
|
271
|
+
if (context.excludedComponentIds.size > 0) {
|
|
272
|
+
const excludedTypes = Array.from(context.excludedComponentIds);
|
|
273
|
+
const placeholders = excludedTypes.map(() => `$${paramIndex++}`).join(', ');
|
|
274
|
+
conditions.push(`NOT EXISTS (SELECT 1 FROM entity_components ec_ex WHERE ec_ex.entity_id = ${partitionTable}.entity_id AND ec_ex.type_id IN (${placeholders}) AND ec_ex.deleted_at IS NULL)`);
|
|
275
|
+
context.params.push(...excludedTypes);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (conditions.length > 0) {
|
|
279
|
+
sql += ` AND ${conditions.join(' AND ')}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Add ordering
|
|
283
|
+
sql += " ORDER BY entity_id";
|
|
284
|
+
|
|
285
|
+
// Add pagination
|
|
286
|
+
if (context.limit !== null) {
|
|
287
|
+
sql += ` LIMIT $${paramIndex++}`;
|
|
288
|
+
context.params.push(context.limit);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (context.offsetValue > 0) {
|
|
292
|
+
sql += ` OFFSET $${paramIndex++}`;
|
|
293
|
+
context.params.push(context.offsetValue);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
context.paramIndex = paramIndex;
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
sql,
|
|
300
|
+
params: context.params,
|
|
301
|
+
context
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public execute(context: QueryContext): QueryResult {
|
|
306
|
+
// Try optimized UNION ALL path for direct partition access
|
|
307
|
+
// This avoids the slow multi-partition scanning by querying each partition directly
|
|
308
|
+
const canUseOptimized = this.canUseUnionAllOptimization() && this.dependencies.length === 0;
|
|
309
|
+
console.log(`OrNode: Using optimized path: ${canUseOptimized}, dependencies: ${this.dependencies.length}, direct partition: ${require("../core/Config").shouldUseDirectPartition()}`);
|
|
310
|
+
console.log(`OrNode: Component types:`, Array.from(this.orQuery.getComponentTypes()));
|
|
311
|
+
|
|
312
|
+
if (canUseOptimized) {
|
|
313
|
+
console.log("OrNode: Using optimized UNION path");
|
|
314
|
+
return this.executeUnionAllOptimized(context);
|
|
315
|
+
}
|
|
316
|
+
console.log("OrNode: Using fallback path");
|
|
317
|
+
|
|
318
|
+
// Fall back to original implementation for:
|
|
319
|
+
// - HASH partitioning (no direct partition access)
|
|
320
|
+
// - Queries with ComponentInclusionNode dependencies
|
|
321
|
+
// - Single component type OR queries
|
|
322
|
+
const branches: string[] = [];
|
|
323
|
+
let paramIndex = context.paramIndex;
|
|
324
|
+
|
|
325
|
+
// Get all component types referenced in the OR query
|
|
326
|
+
const allComponentTypes = this.orQuery.getComponentTypes();
|
|
327
|
+
|
|
328
|
+
// Check if we have ComponentInclusionNode as a dependency
|
|
329
|
+
const hasComponentDependency = this.dependencies.length > 0;
|
|
330
|
+
let baseEntityQuery = "";
|
|
331
|
+
|
|
332
|
+
if (hasComponentDependency) {
|
|
333
|
+
// Get base entities from ComponentInclusionNode
|
|
334
|
+
const componentNode = this.dependencies[0];
|
|
335
|
+
if (componentNode) {
|
|
336
|
+
const baseResult = componentNode.execute(context);
|
|
337
|
+
baseEntityQuery = baseResult.sql;
|
|
338
|
+
paramIndex = baseResult.context.paramIndex;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Build SQL for each branch
|
|
343
|
+
for (const branch of this.orQuery.branches) {
|
|
344
|
+
const componentId = ComponentRegistry.getComponentId(branch.component.name);
|
|
345
|
+
if (!componentId) {
|
|
346
|
+
throw new Error(`Component ${branch.component.name} is not registered`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const componentIdParamIndex = paramIndex;
|
|
350
|
+
let branchSql: string;
|
|
351
|
+
|
|
352
|
+
if (hasComponentDependency) {
|
|
353
|
+
// Filter entities from base query (ComponentInclusionNode returns 'id' column)
|
|
354
|
+
const componentTableName = this.getComponentTableName(componentId);
|
|
355
|
+
branchSql = `
|
|
356
|
+
SELECT base.id as entity_id
|
|
357
|
+
FROM (${baseEntityQuery}) AS base
|
|
358
|
+
WHERE EXISTS (
|
|
359
|
+
SELECT 1 FROM ${componentTableName} c
|
|
360
|
+
WHERE c.entity_id = base.id
|
|
361
|
+
AND c.type_id = $${componentIdParamIndex} AND c.deleted_at IS NULL
|
|
362
|
+
AND c.created_at = (
|
|
363
|
+
SELECT MAX(c2.created_at)
|
|
364
|
+
FROM ${componentTableName} c2
|
|
365
|
+
WHERE c2.entity_id = c.entity_id
|
|
366
|
+
AND c2.type_id = $${componentIdParamIndex} AND c2.deleted_at IS NULL
|
|
367
|
+
)`;
|
|
368
|
+
} else {
|
|
369
|
+
// Use original query without base
|
|
370
|
+
const componentTableName = this.getComponentTableName(componentId);
|
|
371
|
+
branchSql = `
|
|
372
|
+
SELECT ec.entity_id
|
|
373
|
+
FROM entity_components ec
|
|
374
|
+
WHERE ec.type_id = $${componentIdParamIndex} AND ec.deleted_at IS NULL
|
|
375
|
+
AND EXISTS (
|
|
376
|
+
SELECT 1 FROM ${componentTableName} c
|
|
377
|
+
WHERE c.entity_id = ec.entity_id
|
|
378
|
+
AND c.type_id = $${componentIdParamIndex} AND c.deleted_at IS NULL
|
|
379
|
+
AND c.created_at = (
|
|
380
|
+
SELECT MAX(c2.created_at)
|
|
381
|
+
FROM ${componentTableName} c2
|
|
382
|
+
WHERE c2.entity_id = c.entity_id
|
|
383
|
+
AND c2.type_id = $${componentIdParamIndex} AND c2.deleted_at IS NULL
|
|
384
|
+
)`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
context.params.push(componentId);
|
|
388
|
+
paramIndex++;
|
|
389
|
+
|
|
390
|
+
// Add filters for this branch - applied to the latest component data
|
|
391
|
+
const filterConditions: string[] = [];
|
|
392
|
+
if (branch.filters && branch.filters.length > 0) {
|
|
393
|
+
for (const filter of branch.filters) {
|
|
394
|
+
const { field, operator, value } = filter;
|
|
395
|
+
|
|
396
|
+
// Build JSON path for nested properties
|
|
397
|
+
const jsonPath = `c.data->>'${field}'`;
|
|
398
|
+
|
|
399
|
+
switch (operator) {
|
|
400
|
+
case "=":
|
|
401
|
+
case ">":
|
|
402
|
+
case "<":
|
|
403
|
+
case ">=":
|
|
404
|
+
case "<=":
|
|
405
|
+
case "!=":
|
|
406
|
+
if (typeof value === "string") {
|
|
407
|
+
filterConditions.push(`${jsonPath} ${operator} $${paramIndex}`);
|
|
408
|
+
context.params.push(value);
|
|
409
|
+
paramIndex++;
|
|
410
|
+
} else {
|
|
411
|
+
filterConditions.push(`(${jsonPath})::numeric ${operator} $${paramIndex}`);
|
|
412
|
+
context.params.push(value);
|
|
413
|
+
paramIndex++;
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
case "LIKE":
|
|
417
|
+
case "ILIKE":
|
|
418
|
+
filterConditions.push(`${jsonPath} ${operator} $${paramIndex}`);
|
|
419
|
+
context.params.push(value);
|
|
420
|
+
paramIndex++;
|
|
421
|
+
break;
|
|
422
|
+
case "IN":
|
|
423
|
+
if (Array.isArray(value)) {
|
|
424
|
+
const placeholders = value.map(() => `$${paramIndex++}`).join(', ');
|
|
425
|
+
filterConditions.push(`${jsonPath} IN (${placeholders})`);
|
|
426
|
+
context.params.push(...value);
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
case "NOT IN":
|
|
430
|
+
if (Array.isArray(value)) {
|
|
431
|
+
const placeholders = value.map(() => `$${paramIndex++}`).join(', ');
|
|
432
|
+
filterConditions.push(`${jsonPath} NOT IN (${placeholders})`);
|
|
433
|
+
context.params.push(...value);
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
436
|
+
default:
|
|
437
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Apply filters inside the EXISTS/WHERE clause
|
|
443
|
+
if (filterConditions.length > 0) {
|
|
444
|
+
branchSql += ` AND ${filterConditions.join(' AND ')}`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
branchSql += ")";
|
|
448
|
+
|
|
449
|
+
branches.push(branchSql);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Combine branches with UNION
|
|
453
|
+
let sql = `SELECT DISTINCT entity_id as id FROM (${branches.join(' UNION ')}) AS or_results`;
|
|
454
|
+
|
|
455
|
+
// Only ensure entities have ALL components when OrNode is the root (no base requirements)
|
|
456
|
+
// When used as a filter on top of ComponentInclusionNode, base requirements are already ensured
|
|
457
|
+
const componentConditions: string[] = [];
|
|
458
|
+
|
|
459
|
+
if (!hasComponentDependency) {
|
|
460
|
+
for (const componentType of allComponentTypes) {
|
|
461
|
+
const componentId = ComponentRegistry.getComponentId(componentType);
|
|
462
|
+
if (componentId) {
|
|
463
|
+
componentConditions.push(`EXISTS (SELECT 1 FROM entity_components ec_all WHERE ec_all.entity_id = or_results.id AND ec_all.type_id = $${paramIndex} AND ec_all.deleted_at IS NULL)`);
|
|
464
|
+
context.params.push(componentId);
|
|
465
|
+
paramIndex++;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Apply global constraints
|
|
471
|
+
const conditions: string[] = [...componentConditions];
|
|
472
|
+
|
|
473
|
+
// Add entity exclusions
|
|
474
|
+
if (context.excludedEntityIds.size > 0) {
|
|
475
|
+
const excludedIds = Array.from(context.excludedEntityIds);
|
|
476
|
+
const placeholders = excludedIds.map(() => `$${paramIndex++}`).join(', ');
|
|
477
|
+
conditions.push(`entity_id NOT IN (${placeholders})`);
|
|
478
|
+
context.params.push(...excludedIds);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Add component exclusions (entities that have excluded components)
|
|
482
|
+
if (context.excludedComponentIds.size > 0) {
|
|
483
|
+
const excludedTypes = Array.from(context.excludedComponentIds);
|
|
484
|
+
const placeholders = excludedTypes.map(() => `$${paramIndex++}`).join(', ');
|
|
485
|
+
conditions.push(`NOT EXISTS (SELECT 1 FROM entity_components ec_ex WHERE ec_ex.entity_id = or_results.id AND ec_ex.type_id IN (${placeholders}) AND ec_ex.deleted_at IS NULL)`);
|
|
486
|
+
context.params.push(...excludedTypes);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (conditions.length > 0) {
|
|
490
|
+
sql += ` WHERE ${conditions.join(' AND ')}`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Add ordering
|
|
494
|
+
sql += " ORDER BY entity_id";
|
|
495
|
+
|
|
496
|
+
// Add pagination
|
|
497
|
+
if (context.limit !== null) {
|
|
498
|
+
sql += ` LIMIT $${paramIndex++}`;
|
|
499
|
+
context.params.push(context.limit);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (context.offsetValue > 0) {
|
|
503
|
+
sql += ` OFFSET $${paramIndex++}`;
|
|
504
|
+
context.params.push(context.offsetValue);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
context.paramIndex = paramIndex;
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
sql,
|
|
511
|
+
params: context.params,
|
|
512
|
+
context
|
|
513
|
+
};
|
|
514
|
+
} public getNodeType(): string {
|
|
515
|
+
return "OrNode";
|
|
516
|
+
}
|
|
517
|
+
}
|
package/query/OrQuery.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BaseComponent } from "../core/components";
|
|
2
|
+
import type { QueryFilter } from "./Query";
|
|
3
|
+
|
|
4
|
+
export interface OrBranch {
|
|
5
|
+
component: new (...args: any[]) => BaseComponent;
|
|
6
|
+
filters?: QueryFilter[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents an OR query with multiple branches
|
|
11
|
+
* Each branch specifies a component and optional filters
|
|
12
|
+
* An entity matches if it satisfies ANY of the branches
|
|
13
|
+
*/
|
|
14
|
+
export class OrQuery {
|
|
15
|
+
public branches: OrBranch[];
|
|
16
|
+
|
|
17
|
+
constructor(branches: OrBranch[]) {
|
|
18
|
+
if (!branches || branches.length === 0) {
|
|
19
|
+
throw new Error("OR query must have at least one branch");
|
|
20
|
+
}
|
|
21
|
+
this.branches = branches;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get all component types used in this OR query
|
|
26
|
+
*/
|
|
27
|
+
public getComponentTypes(): Set<string> {
|
|
28
|
+
const types = new Set<string>();
|
|
29
|
+
for (const branch of this.branches) {
|
|
30
|
+
// We'll resolve component IDs when we have access to ComponentRegistry
|
|
31
|
+
types.add(branch.component.name);
|
|
32
|
+
}
|
|
33
|
+
return types;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if this OR query has any filters
|
|
38
|
+
*/
|
|
39
|
+
public hasFilters(): boolean {
|
|
40
|
+
return this.branches.some(branch => branch.filters && branch.filters.length > 0);
|
|
41
|
+
}
|
|
42
|
+
}
|