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.
Files changed (257) hide show
  1. package/.claude/settings.local.json +47 -0
  2. package/.claude/skills/update-memory.md +74 -0
  3. package/.prettierrc +4 -0
  4. package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
  5. package/.serena/memories/architecture.md +154 -0
  6. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
  7. package/.serena/memories/code_style_and_conventions.md +76 -0
  8. package/.serena/memories/project_overview.md +43 -0
  9. package/.serena/memories/schema-dsl-plan.md +107 -0
  10. package/.serena/memories/suggested_commands.md +80 -0
  11. package/.serena/memories/typescript-compilation-status.md +54 -0
  12. package/.serena/project.yml +114 -0
  13. package/TODO.md +1 -7
  14. package/bun.lock +150 -4
  15. package/bunfig.toml +10 -0
  16. package/config/cache.config.ts +77 -0
  17. package/config/upload.config.ts +4 -5
  18. package/core/App.ts +870 -123
  19. package/core/ArcheType.ts +2268 -377
  20. package/core/BatchLoader.ts +181 -71
  21. package/core/Config.ts +153 -0
  22. package/core/Decorators.ts +4 -1
  23. package/core/Entity.ts +621 -92
  24. package/core/EntityHookManager.ts +1 -1
  25. package/core/EntityInterface.ts +3 -1
  26. package/core/EntityManager.ts +1 -13
  27. package/core/ErrorHandler.ts +8 -2
  28. package/core/Logger.ts +9 -0
  29. package/core/Middleware.ts +34 -0
  30. package/core/RequestContext.ts +5 -1
  31. package/core/RequestLoaders.ts +227 -93
  32. package/core/SchedulerManager.ts +193 -52
  33. package/core/cache/CacheAnalytics.ts +399 -0
  34. package/core/cache/CacheFactory.ts +145 -0
  35. package/core/cache/CacheManager.ts +520 -0
  36. package/core/cache/CacheProvider.ts +34 -0
  37. package/core/cache/CacheWarmer.ts +157 -0
  38. package/core/cache/CompressionUtils.ts +110 -0
  39. package/core/cache/MemoryCache.ts +251 -0
  40. package/core/cache/MultiLevelCache.ts +180 -0
  41. package/core/cache/NoOpCache.ts +53 -0
  42. package/core/cache/RedisCache.ts +464 -0
  43. package/core/cache/TTLStrategy.ts +254 -0
  44. package/core/cache/index.ts +6 -0
  45. package/core/components/BaseComponent.ts +120 -0
  46. package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
  47. package/core/components/Decorators.ts +88 -0
  48. package/core/components/Interfaces.ts +7 -0
  49. package/core/components/index.ts +5 -0
  50. package/core/decorators/EntityHooks.ts +0 -3
  51. package/core/decorators/IndexedField.ts +26 -0
  52. package/core/decorators/ScheduledTask.ts +0 -47
  53. package/core/events/EntityLifecycleEvents.ts +1 -1
  54. package/core/health.ts +112 -0
  55. package/core/metadata/definitions/ArcheType.ts +14 -0
  56. package/core/metadata/definitions/Component.ts +9 -0
  57. package/core/metadata/definitions/gqlObject.ts +1 -1
  58. package/core/metadata/index.ts +42 -1
  59. package/core/metadata/metadata-storage.ts +28 -2
  60. package/core/middleware/AccessLog.ts +59 -0
  61. package/core/middleware/RequestId.ts +38 -0
  62. package/core/middleware/SecurityHeaders.ts +62 -0
  63. package/core/middleware/index.ts +3 -0
  64. package/core/scheduler/DistributedLock.ts +266 -0
  65. package/core/scheduler/index.ts +15 -0
  66. package/core/validateEnv.ts +92 -0
  67. package/database/DatabaseHelper.ts +416 -40
  68. package/database/IndexingStrategy.ts +342 -0
  69. package/database/PreparedStatementCache.ts +226 -0
  70. package/database/index.ts +32 -7
  71. package/database/sqlHelpers.ts +14 -2
  72. package/endpoints/archetypes.ts +362 -0
  73. package/endpoints/components.ts +58 -0
  74. package/endpoints/entity.ts +80 -0
  75. package/endpoints/index.ts +27 -0
  76. package/endpoints/query.ts +93 -0
  77. package/endpoints/stats.ts +76 -0
  78. package/endpoints/tables.ts +212 -0
  79. package/endpoints/types.ts +155 -0
  80. package/gql/ArchetypeOperations.ts +32 -86
  81. package/gql/Generator.ts +27 -315
  82. package/gql/GeneratorV2.ts +37 -0
  83. package/gql/builders/InputTypeBuilder.ts +99 -0
  84. package/gql/builders/ResolverBuilder.ts +234 -0
  85. package/gql/builders/TypeDefBuilder.ts +105 -0
  86. package/gql/builders/index.ts +3 -0
  87. package/gql/decorators/Upload.ts +1 -1
  88. package/gql/depthLimit.ts +85 -0
  89. package/gql/graph/GraphNode.ts +224 -0
  90. package/gql/graph/SchemaGraph.ts +278 -0
  91. package/gql/helpers.ts +8 -2
  92. package/gql/index.ts +56 -4
  93. package/gql/middleware.ts +79 -0
  94. package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
  95. package/gql/orchestration/index.ts +1 -0
  96. package/gql/scanner/ServiceScanner.ts +347 -0
  97. package/gql/schema/index.ts +458 -0
  98. package/gql/strategies/TypeGenerationStrategy.ts +329 -0
  99. package/gql/types.ts +1 -0
  100. package/gql/utils/TypeSignature.ts +220 -0
  101. package/gql/utils/index.ts +1 -0
  102. package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
  103. package/gql/visitors/DeduplicationVisitor.ts +82 -0
  104. package/gql/visitors/GraphVisitor.ts +78 -0
  105. package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
  106. package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
  107. package/gql/visitors/TypeCollectorVisitor.ts +79 -0
  108. package/gql/visitors/VisitorComposer.ts +96 -0
  109. package/gql/visitors/index.ts +7 -0
  110. package/package.json +59 -37
  111. package/plugins/index.ts +2 -2
  112. package/query/CTENode.ts +97 -0
  113. package/query/ComponentInclusionNode.ts +689 -0
  114. package/query/FilterBuilder.ts +127 -0
  115. package/query/FilterBuilderRegistry.ts +202 -0
  116. package/query/OrNode.ts +517 -0
  117. package/query/OrQuery.ts +42 -0
  118. package/query/Query.ts +1022 -0
  119. package/query/QueryContext.ts +170 -0
  120. package/query/QueryDAG.ts +122 -0
  121. package/query/QueryNode.ts +65 -0
  122. package/query/SourceNode.ts +53 -0
  123. package/query/builders/FullTextSearchBuilder.ts +236 -0
  124. package/query/index.ts +21 -0
  125. package/scheduler/index.ts +40 -8
  126. package/service/Service.ts +2 -1
  127. package/service/ServiceRegistry.ts +6 -5
  128. package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
  129. package/storage/S3StorageProvider.ts +316 -0
  130. package/{core/storage → storage}/StorageProvider.ts +7 -3
  131. package/studio/bun.lock +482 -0
  132. package/studio/index.html +13 -0
  133. package/studio/package.json +39 -0
  134. package/studio/postcss.config.js +6 -0
  135. package/studio/src/components/DataTable.tsx +211 -0
  136. package/studio/src/components/Layout.tsx +13 -0
  137. package/studio/src/components/PageContainer.tsx +9 -0
  138. package/studio/src/components/PageHeader.tsx +13 -0
  139. package/studio/src/components/SearchBar.tsx +57 -0
  140. package/studio/src/components/Sidebar.tsx +294 -0
  141. package/studio/src/components/ui/button.tsx +56 -0
  142. package/studio/src/components/ui/checkbox.tsx +26 -0
  143. package/studio/src/components/ui/input.tsx +25 -0
  144. package/studio/src/hooks/useDataTable.ts +131 -0
  145. package/studio/src/index.css +36 -0
  146. package/studio/src/lib/api.ts +186 -0
  147. package/studio/src/lib/utils.ts +13 -0
  148. package/studio/src/main.tsx +17 -0
  149. package/studio/src/pages/ArcheType.tsx +239 -0
  150. package/studio/src/pages/Components.tsx +124 -0
  151. package/studio/src/pages/EntityInspector.tsx +302 -0
  152. package/studio/src/pages/QueryRunner.tsx +246 -0
  153. package/studio/src/pages/Table.tsx +94 -0
  154. package/studio/src/pages/Welcome.tsx +241 -0
  155. package/studio/src/routes.tsx +45 -0
  156. package/studio/src/store/archeTypeSettings.ts +30 -0
  157. package/studio/src/store/studio.ts +65 -0
  158. package/studio/src/utils/columnHelpers.tsx +114 -0
  159. package/studio/studio-instructions.md +81 -0
  160. package/studio/tailwind.config.js +77 -0
  161. package/studio/tsconfig.json +24 -0
  162. package/studio/utils.ts +54 -0
  163. package/studio/vite.config.js +19 -0
  164. package/swagger/generator.ts +1 -1
  165. package/tests/e2e/http.test.ts +126 -0
  166. package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
  167. package/tests/fixtures/components/TestOrder.ts +23 -0
  168. package/tests/fixtures/components/TestProduct.ts +23 -0
  169. package/tests/fixtures/components/TestUser.ts +20 -0
  170. package/tests/fixtures/components/index.ts +6 -0
  171. package/tests/graphql/SchemaGeneration.test.ts +90 -0
  172. package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
  173. package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
  174. package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
  175. package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
  176. package/tests/integration/entity/Entity.persistence.test.ts +333 -0
  177. package/tests/integration/query/Query.exec.test.ts +523 -0
  178. package/tests/pglite-setup.ts +61 -0
  179. package/tests/setup.ts +164 -0
  180. package/tests/stress/BenchmarkRunner.ts +203 -0
  181. package/tests/stress/DataSeeder.ts +190 -0
  182. package/tests/stress/StressTestReporter.ts +229 -0
  183. package/tests/stress/cursor-perf-test.ts +171 -0
  184. package/tests/stress/fixtures/StressTestComponents.ts +58 -0
  185. package/tests/stress/index.ts +7 -0
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
  187. package/tests/unit/BatchLoader.test.ts +82 -0
  188. package/tests/unit/archetype/ArcheType.test.ts +107 -0
  189. package/tests/unit/cache/CacheManager.test.ts +347 -0
  190. package/tests/unit/cache/MemoryCache.test.ts +260 -0
  191. package/tests/unit/cache/RedisCache.test.ts +411 -0
  192. package/tests/unit/entity/Entity.components.test.ts +244 -0
  193. package/tests/unit/entity/Entity.test.ts +345 -0
  194. package/tests/unit/gql/depthLimit.test.ts +203 -0
  195. package/tests/unit/gql/operationMiddleware.test.ts +293 -0
  196. package/tests/unit/health/Health.test.ts +129 -0
  197. package/tests/unit/middleware/AccessLog.test.ts +37 -0
  198. package/tests/unit/middleware/Middleware.test.ts +98 -0
  199. package/tests/unit/middleware/RequestId.test.ts +54 -0
  200. package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
  201. package/tests/unit/query/FilterBuilder.test.ts +111 -0
  202. package/tests/unit/query/Query.test.ts +308 -0
  203. package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
  204. package/tests/unit/schema/schema-integration.test.ts +426 -0
  205. package/tests/unit/schema/schema.test.ts +580 -0
  206. package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
  207. package/tests/unit/upload/RestUpload.test.ts +267 -0
  208. package/tests/unit/validateEnv.test.ts +82 -0
  209. package/tests/utils/entity-tracker.ts +57 -0
  210. package/tests/utils/index.ts +13 -0
  211. package/tests/utils/test-context.ts +149 -0
  212. package/tsconfig.json +5 -1
  213. package/types/archetype.types.ts +6 -0
  214. package/types/hooks.types.ts +1 -1
  215. package/types/query.types.ts +110 -0
  216. package/types/scheduler.types.ts +68 -7
  217. package/types/upload.types.ts +1 -0
  218. package/{core → upload}/FileValidator.ts +10 -1
  219. package/upload/RestUpload.ts +130 -0
  220. package/{core/components → upload}/UploadComponent.ts +11 -11
  221. package/{core → upload}/UploadManager.ts +3 -3
  222. package/upload/index.ts +23 -7
  223. package/utils/UploadHelper.ts +27 -6
  224. package/utils/cronParser.ts +16 -6
  225. package/.github/workflows/deploy-docs.yml +0 -57
  226. package/core/Components.ts +0 -202
  227. package/core/EntityCache.ts +0 -15
  228. package/core/Query.ts +0 -880
  229. package/docs/README.md +0 -149
  230. package/docs/_coverpage.md +0 -36
  231. package/docs/_sidebar.md +0 -23
  232. package/docs/api/core.md +0 -568
  233. package/docs/api/hooks.md +0 -554
  234. package/docs/api/index.md +0 -222
  235. package/docs/api/query.md +0 -678
  236. package/docs/api/service.md +0 -744
  237. package/docs/core-concepts/archetypes.md +0 -512
  238. package/docs/core-concepts/components.md +0 -498
  239. package/docs/core-concepts/entity.md +0 -314
  240. package/docs/core-concepts/hooks.md +0 -683
  241. package/docs/core-concepts/query.md +0 -588
  242. package/docs/core-concepts/services.md +0 -647
  243. package/docs/examples/code-examples.md +0 -425
  244. package/docs/getting-started.md +0 -337
  245. package/docs/index.html +0 -97
  246. package/tests/bench/insert.bench.ts +0 -60
  247. package/tests/bench/relations.bench.ts +0 -270
  248. package/tests/bench/sorting.bench.ts +0 -416
  249. package/tests/component-hooks-simple.test.ts +0 -117
  250. package/tests/component-hooks.test.ts +0 -1461
  251. package/tests/component.test.ts +0 -339
  252. package/tests/errorHandling.test.ts +0 -155
  253. package/tests/hooks.test.ts +0 -667
  254. package/tests/query-sorting.test.ts +0 -101
  255. package/tests/query.test.ts +0 -81
  256. package/tests/relations.test.ts +0 -170
  257. package/tests/scheduler.test.ts +0 -724
@@ -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
+ }
@@ -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
+ }