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,689 @@
1
+ import { QueryNode } from "./QueryNode";
2
+ import type { QueryResult } from "./QueryNode";
3
+ import { QueryContext } from "./QueryContext";
4
+ import { shouldUseLateralJoins, shouldUseDirectPartition } from "../core/Config";
5
+ import { FilterBuilderRegistry } from "./FilterBuilderRegistry";
6
+ import {ComponentRegistry} from "../core/components";
7
+
8
+ export class ComponentInclusionNode extends QueryNode {
9
+ private getComponentTableName(compId: string): string {
10
+ if (shouldUseDirectPartition()) {
11
+ return ComponentRegistry.getPartitionTableName(compId) || 'components';
12
+ }
13
+ return 'components';
14
+ }
15
+
16
+ public execute(context: QueryContext): QueryResult {
17
+ const componentIds = Array.from(context.componentIds);
18
+ const excludedIds = Array.from(context.excludedComponentIds);
19
+
20
+ if (componentIds.length === 0) {
21
+ // No components required, return the input as-is
22
+ return {
23
+ sql: "",
24
+ params: context.params,
25
+ context
26
+ };
27
+ }
28
+
29
+ let sql = "";
30
+ const componentCount = componentIds.length;
31
+ const useLateralJoins = Boolean(shouldUseLateralJoins());
32
+
33
+ // Check if CTE is available and use it to avoid redundant entity_components scans
34
+ const useCTE = Boolean(context.hasCTE && context.cteName);
35
+
36
+ // Collect LATERAL join fragments if using LATERAL joins
37
+ const lateralJoins: string[] = [];
38
+ const lateralConditions: string[] = [];
39
+
40
+ // Check if we need custom sorting (sortOrders specified)
41
+ const hasSortOrders = context.sortOrders.length > 0;
42
+
43
+ if (componentCount === 1) {
44
+ // Single component case
45
+ const componentId = componentIds[0]!;
46
+
47
+ // Check if we can use single-pass optimization (filter + sort on same component)
48
+ // This must be checked BEFORE adding any params to avoid orphan params
49
+ const canUseSinglePass = hasSortOrders &&
50
+ context.sortOrders.length === 1 &&
51
+ context.componentFilters.size > 0 &&
52
+ !context.withId &&
53
+ excludedIds.length === 0 &&
54
+ context.excludedEntityIds.size === 0 &&
55
+ !useCTE;
56
+
57
+ if (canUseSinglePass) {
58
+ const singlePass = this.applySinglePassFilterSort(context);
59
+ if (singlePass) {
60
+ // Single-pass handles filters, sort, and pagination all in one query
61
+ return { sql: singlePass, params: context.params, context };
62
+ }
63
+ }
64
+
65
+ if (useCTE) {
66
+ // Use CTE for base entity filtering
67
+ sql = `SELECT DISTINCT ${context.cteName}.entity_id as id FROM ${context.cteName}`;
68
+
69
+ // Filter by the specific component type if not already in CTE
70
+ if (!componentIds.some(id => context.componentIds.has(id))) {
71
+ sql += ` WHERE EXISTS (
72
+ SELECT 1 FROM entity_components ec
73
+ WHERE ec.entity_id = ${context.cteName}.entity_id
74
+ AND ec.type_id = $${context.addParam(componentId)}::text
75
+ AND ec.deleted_at IS NULL
76
+ )`;
77
+ }
78
+ } else {
79
+ sql = `SELECT DISTINCT ec.entity_id as id FROM entity_components ec WHERE ec.type_id = $${context.addParam(componentId)}::text AND ec.deleted_at IS NULL`;
80
+ }
81
+
82
+ if (context.withId) {
83
+ const tableAlias = useCTE ? context.cteName : "ec";
84
+ const whereKeyword = sql.includes('WHERE') ? 'AND' : 'WHERE';
85
+ sql += ` ${whereKeyword} ${tableAlias}.entity_id = $${context.addParam(context.withId)}`;
86
+ }
87
+
88
+ // Add exclusions
89
+ if (excludedIds.length > 0) {
90
+ const tableAlias = useCTE ? context.cteName : "ec";
91
+ const whereKeyword = sql.includes('WHERE') ? 'AND' : 'WHERE';
92
+ const excludedPlaceholders = excludedIds.map((id) => `$${context.addParam(id)}`).join(', ');
93
+ sql += ` ${whereKeyword} NOT EXISTS (
94
+ SELECT 1 FROM entity_components ec_ex
95
+ WHERE ec_ex.entity_id = ${tableAlias}.entity_id
96
+ AND ec_ex.type_id IN (${excludedPlaceholders})
97
+ AND ec_ex.deleted_at IS NULL
98
+ )`;
99
+ }
100
+
101
+ // Add entity exclusions
102
+ if (context.excludedEntityIds.size > 0) {
103
+ const tableAlias = useCTE ? context.cteName : "ec";
104
+ const whereKeyword = sql.includes('WHERE') ? 'AND' : 'WHERE';
105
+ const entityExcludedIds = Array.from(context.excludedEntityIds);
106
+ const entityPlaceholders = entityExcludedIds.map((id) => `$${context.addParam(id)}`).join(', ');
107
+ sql += ` ${whereKeyword} ${tableAlias}.entity_id NOT IN (${entityPlaceholders})`;
108
+ }
109
+
110
+ // Apply component filters for single component (normal path)
111
+ sql = this.applyComponentFilters(context, componentIds, useCTE, useLateralJoins, lateralJoins, lateralConditions, sql, new Map());
112
+
113
+ // Apply sorting with component data joins if sortOrders are specified
114
+ if (hasSortOrders) {
115
+ sql = this.applySortingWithComponentJoins(sql, context);
116
+ } else {
117
+ // Default: order by entity_id
118
+ const tableAlias = useCTE ? context.cteName : "ec";
119
+ const idColumn = useCTE ? `${context.cteName}.entity_id` : `${tableAlias}.entity_id`;
120
+
121
+ // Apply cursor-based pagination if cursor is set (more efficient than OFFSET)
122
+ if (context.cursorId !== null && !context.paginationAppliedInCTE) {
123
+ const operator = context.cursorDirection === 'after' ? '>' : '<';
124
+ const whereKeyword = sql.includes('WHERE') ? 'AND' : 'WHERE';
125
+ sql += ` ${whereKeyword} ${idColumn} ${operator} $${context.addParam(context.cursorId)}`;
126
+ }
127
+
128
+ // Order direction depends on cursor direction
129
+ const orderDirection = context.cursorDirection === 'before' ? 'DESC' : 'ASC';
130
+ sql += ` ORDER BY ${idColumn} ${orderDirection}`;
131
+
132
+ // Add LIMIT and OFFSET only if not already applied in CTE
133
+ // When pagination is applied at CTE level, skip it here to avoid double pagination
134
+ if (!context.paginationAppliedInCTE) {
135
+ if (context.limit !== null) {
136
+ sql += ` LIMIT $${context.addParam(context.limit)}`;
137
+ }
138
+ // Only add OFFSET when not using cursor-based pagination
139
+ if (context.cursorId === null && (context.offsetValue > 0 || context.limit !== null)) {
140
+ sql += ` OFFSET $${context.addParam(context.offsetValue)}`;
141
+ }
142
+ }
143
+ }
144
+ } else {
145
+ // Multiple components case
146
+ // Create parameter indices for component IDs to avoid duplicates
147
+ const componentParamIndices: Map<string, number> = new Map();
148
+ const componentPlaceholders = componentIds.map((id) => {
149
+ if (!componentParamIndices.has(id)) {
150
+ componentParamIndices.set(id, context.addParam(id));
151
+ }
152
+ return `$${componentParamIndices.get(id)}::text`;
153
+ }).join(', ');
154
+
155
+ if (useCTE) {
156
+ // Use CTE for base entity filtering
157
+ sql = `SELECT DISTINCT ${context.cteName}.entity_id as id FROM ${context.cteName}`;
158
+
159
+ // Ensure all required components are present
160
+ sql += ` WHERE (`;
161
+ const componentChecks = componentIds.map(compId => {
162
+ if (!componentParamIndices.has(compId)) {
163
+ componentParamIndices.set(compId, context.addParam(compId));
164
+ }
165
+ return `EXISTS (
166
+ SELECT 1 FROM entity_components ec
167
+ WHERE ec.entity_id = ${context.cteName}.entity_id
168
+ AND ec.type_id = $${componentParamIndices.get(compId)}::text
169
+ AND ec.deleted_at IS NULL
170
+ )`;
171
+ });
172
+ sql += componentChecks.join(' AND ') + `)`;
173
+ } else {
174
+ sql = `SELECT DISTINCT ec.entity_id as id FROM entity_components ec WHERE ec.type_id IN (${componentPlaceholders}) AND ec.deleted_at IS NULL`;
175
+ }
176
+
177
+ if (context.withId) {
178
+ const tableAlias = useCTE ? context.cteName : "ec";
179
+ const whereKeyword = sql.includes('WHERE') ? 'AND' : 'WHERE';
180
+ sql += ` ${whereKeyword} ${tableAlias}.entity_id = $${context.addParam(context.withId)}`;
181
+ }
182
+
183
+ // Add exclusions
184
+ if (excludedIds.length > 0) {
185
+ const tableAlias = useCTE ? context.cteName : "ec";
186
+ const whereKeyword = sql.includes('WHERE') ? 'AND' : 'WHERE';
187
+ const excludedPlaceholders = excludedIds.map((id) => `$${context.addParam(id)}`).join(', ');
188
+ sql += ` ${whereKeyword} NOT EXISTS (
189
+ SELECT 1 FROM entity_components ec_ex
190
+ WHERE ec_ex.entity_id = ${tableAlias}.entity_id
191
+ AND ec_ex.type_id IN (${excludedPlaceholders})
192
+ AND ec_ex.deleted_at IS NULL
193
+ )`;
194
+ }
195
+
196
+ // Add entity exclusions
197
+ if (context.excludedEntityIds.size > 0) {
198
+ const tableAlias = useCTE ? context.cteName : "ec";
199
+ const whereKeyword = sql.includes('WHERE') ? 'AND' : 'WHERE';
200
+ const entityExcludedIds = Array.from(context.excludedEntityIds);
201
+ const entityPlaceholders = entityExcludedIds.map((id) => `$${context.addParam(id)}`).join(', ');
202
+ sql += ` ${whereKeyword} ${tableAlias}.entity_id NOT IN (${entityPlaceholders})`;
203
+ }
204
+
205
+ // Apply component filters for multiple components
206
+ sql = this.applyComponentFilters(context, componentIds, useCTE, useLateralJoins, lateralJoins, lateralConditions, sql, componentParamIndices);
207
+
208
+ if (!useCTE) {
209
+ sql += ` GROUP BY ec.entity_id HAVING COUNT(DISTINCT ec.type_id) = $${context.addParam(componentCount)}`;
210
+ }
211
+
212
+ // Apply sorting with component data joins if sortOrders are specified
213
+ if (hasSortOrders) {
214
+ sql = this.applySortingWithComponentJoins(sql, context);
215
+ } else {
216
+ // Default: order by entity_id
217
+ const tableAlias = useCTE ? context.cteName : "ec";
218
+ const idColumn = useCTE ? `${context.cteName}.entity_id` : `${tableAlias}.entity_id`;
219
+
220
+ // Apply cursor-based pagination if cursor is set (more efficient than OFFSET)
221
+ if (context.cursorId !== null && !context.paginationAppliedInCTE) {
222
+ const operator = context.cursorDirection === 'after' ? '>' : '<';
223
+ const whereKeyword = sql.includes('WHERE') ? 'AND' : 'WHERE';
224
+ sql += ` ${whereKeyword} ${idColumn} ${operator} $${context.addParam(context.cursorId)}`;
225
+ }
226
+
227
+ // Order direction depends on cursor direction
228
+ const orderDirection = context.cursorDirection === 'before' ? 'DESC' : 'ASC';
229
+ sql += ` ORDER BY ${idColumn} ${orderDirection}`;
230
+
231
+ // Add LIMIT and OFFSET only if not already applied in CTE
232
+ // When pagination is applied at CTE level, skip it here to avoid double pagination
233
+ if (!context.paginationAppliedInCTE) {
234
+ if (context.limit !== null) {
235
+ sql += ` LIMIT $${context.addParam(context.limit)}`;
236
+ }
237
+ // Only add OFFSET when not using cursor-based pagination
238
+ if (context.cursorId === null && (context.offsetValue > 0 || context.limit !== null)) {
239
+ sql += ` OFFSET $${context.addParam(context.offsetValue)}`;
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ return {
246
+ sql,
247
+ params: context.params,
248
+ context
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Wrap the base query with sorting joins and apply ORDER BY, LIMIT, OFFSET
254
+ * This ensures that sorting and pagination work together correctly
255
+ */
256
+ private applySortingWithComponentJoins(baseQuery: string, context: QueryContext): string {
257
+ // Check if we can use the optimized direct partition sort
258
+ if (shouldUseDirectPartition() && context.sortOrders.length === 1) {
259
+ const optimized = this.applySortingOptimized(baseQuery, context);
260
+ if (optimized) return optimized;
261
+ }
262
+
263
+ // Try single-pass optimization when filters and sort are on the same component
264
+ if (context.sortOrders.length === 1) {
265
+ const singlePass = this.applySinglePassFilterSort(context);
266
+ if (singlePass) return singlePass;
267
+ }
268
+
269
+ // Wrap the base query as a subquery to get entity ids
270
+ let sql = `SELECT base_entities.id FROM (${baseQuery}) AS base_entities`;
271
+
272
+ // Build LEFT JOINs for each sort order to access component data
273
+ const sortJoins: string[] = [];
274
+ const orderByClauses: string[] = [];
275
+
276
+ for (let i = 0; i < context.sortOrders.length; i++) {
277
+ const sortOrder = context.sortOrders[i]!;
278
+ const sortAlias = `sort_${i}`;
279
+ const compAlias = `comp_${i}`;
280
+
281
+ // Get the component type ID for this sort order
282
+ const typeId = ComponentRegistry.getComponentId(sortOrder.component);
283
+ if (!typeId) {
284
+ continue; // Skip if component not registered
285
+ }
286
+
287
+ // LEFT JOIN to entity_components and components to get the sort data
288
+ const sortComponentTableName = this.getComponentTableName(typeId);
289
+ sortJoins.push(`
290
+ LEFT JOIN entity_components ${sortAlias}
291
+ ON ${sortAlias}.entity_id = base_entities.id
292
+ AND ${sortAlias}.type_id = $${context.addParam(typeId)}::text
293
+ AND ${sortAlias}.deleted_at IS NULL
294
+ LEFT JOIN ${sortComponentTableName} ${compAlias}
295
+ ON ${compAlias}.id = ${sortAlias}.component_id
296
+ AND ${compAlias}.deleted_at IS NULL`);
297
+
298
+ // Build ORDER BY clause for this sort order
299
+ // Access the property from JSONB data
300
+ const nullsClause = sortOrder.nullsFirst ? 'NULLS FIRST' : 'NULLS LAST';
301
+ orderByClauses.push(`${compAlias}.data->>'${sortOrder.property}' ${sortOrder.direction} ${nullsClause}`);
302
+ }
303
+
304
+ // Combine joins
305
+ sql += sortJoins.join('');
306
+
307
+ // Add ORDER BY clause
308
+ if (orderByClauses.length > 0) {
309
+ sql += ` ORDER BY ${orderByClauses.join(', ')}`;
310
+ } else {
311
+ // Fallback to entity id if no valid sort orders
312
+ sql += ` ORDER BY base_entities.id`;
313
+ }
314
+
315
+ // Add LIMIT and OFFSET only if not already applied in CTE
316
+ // When pagination is applied at CTE level, skip it here to avoid double pagination
317
+ if (!context.paginationAppliedInCTE) {
318
+ if (context.limit !== null) {
319
+ sql += ` LIMIT $${context.addParam(context.limit)}`;
320
+ }
321
+ // Only add OFFSET when not using cursor-based pagination
322
+ if (context.cursorId === null && (context.offsetValue > 0 || context.limit !== null)) {
323
+ sql += ` OFFSET $${context.addParam(context.offsetValue)}`;
324
+ }
325
+ }
326
+
327
+ return sql;
328
+ }
329
+
330
+ /**
331
+ * Single-pass optimization when all filters and sort are on the same component.
332
+ * Instead of: CTE -> EXISTS filters -> subquery -> JOIN for sort -> LIMIT
333
+ * We do: JOIN once -> filter + sort in same query -> LIMIT
334
+ *
335
+ * This is dramatically faster because PostgreSQL can use indexes to find
336
+ * the top N matching rows directly instead of finding ALL matches first.
337
+ */
338
+ private applySinglePassFilterSort(context: QueryContext): string | null {
339
+ if (context.sortOrders.length !== 1) return null;
340
+
341
+ const sortOrder = context.sortOrders[0]!;
342
+ const sortTypeId = ComponentRegistry.getComponentId(sortOrder.component);
343
+ if (!sortTypeId) return null;
344
+
345
+ // Check if all filters are on the same component as the sort
346
+ const filterComponentIds = Array.from(context.componentFilters.keys());
347
+ if (filterComponentIds.length === 0) return null;
348
+ if (filterComponentIds.length > 1) return null; // Multiple components - can't optimize
349
+ if (filterComponentIds[0] !== sortTypeId) return null; // Filter and sort on different components
350
+
351
+ // All filters and sort are on the same component - use single-pass optimization
352
+ const filters = context.componentFilters.get(sortTypeId) || [];
353
+ if (filters.length === 0) return null;
354
+
355
+ const componentTableName = this.getComponentTableName(sortTypeId);
356
+ const useDirectPartition = shouldUseDirectPartition() && componentTableName !== 'components';
357
+
358
+ // Build filter conditions
359
+ const filterConditions: string[] = [];
360
+ for (const filter of filters) {
361
+ // Build JSON path
362
+ let jsonPath: string;
363
+ if (filter.field.includes('.')) {
364
+ const parts = filter.field.split('.');
365
+ const lastPart = parts.pop()!;
366
+ const nestedPath = parts.map(p => `'${p}'`).join('->');
367
+ jsonPath = `c.data->${nestedPath}->>'${lastPart}'`;
368
+ } else {
369
+ jsonPath = `c.data->>'${filter.field}'`;
370
+ }
371
+
372
+ // Build condition based on type
373
+ let condition: string;
374
+ if (typeof filter.value === 'number') {
375
+ condition = `(${jsonPath})::numeric ${filter.operator} $${context.addParam(filter.value)}::numeric`;
376
+ } else if (typeof filter.value === 'boolean') {
377
+ condition = `(${jsonPath})::boolean ${filter.operator} $${context.addParam(filter.value)}`;
378
+ } else if (filter.operator === 'IN' || filter.operator === 'NOT IN') {
379
+ if (Array.isArray(filter.value)) {
380
+ const placeholders = filter.value.map((v: any) => `$${context.addParam(v)}`).join(', ');
381
+ condition = `${jsonPath} ${filter.operator} (${placeholders})`;
382
+ } else {
383
+ return null; // Invalid - fall back to normal path
384
+ }
385
+ } else if (filter.operator === 'LIKE' || filter.operator === 'NOT LIKE' || filter.operator === 'ILIKE') {
386
+ condition = `${jsonPath} ${filter.operator} $${context.addParam(filter.value)}::text`;
387
+ } else {
388
+ condition = `${jsonPath} ${filter.operator} $${context.addParam(filter.value)}::text`;
389
+ }
390
+
391
+ filterConditions.push(condition);
392
+ }
393
+
394
+ const nullsClause = sortOrder.nullsFirst ? 'NULLS FIRST' : 'NULLS LAST';
395
+
396
+ let sql: string;
397
+ if (useDirectPartition) {
398
+ // Direct partition access - most efficient
399
+ // No DISTINCT needed since each entity has one component of this type
400
+ sql = `SELECT c.entity_id as id FROM ${componentTableName} c
401
+ WHERE c.type_id = $${context.addParam(sortTypeId)}::text
402
+ AND c.deleted_at IS NULL
403
+ AND ${filterConditions.join(' AND ')}
404
+ ORDER BY c.data->>'${sortOrder.property}' ${sortOrder.direction} ${nullsClause}`;
405
+ } else {
406
+ // Use entity_components junction
407
+ // No DISTINCT needed since each entity has one component of this type
408
+ sql = `SELECT ec.entity_id as id FROM entity_components ec
409
+ JOIN ${componentTableName} c ON c.id = ec.component_id AND c.deleted_at IS NULL
410
+ WHERE ec.type_id = $${context.addParam(sortTypeId)}::text
411
+ AND ec.deleted_at IS NULL
412
+ AND ${filterConditions.join(' AND ')}
413
+ ORDER BY c.data->>'${sortOrder.property}' ${sortOrder.direction} ${nullsClause}`;
414
+ }
415
+
416
+ // Add pagination
417
+ if (!context.paginationAppliedInCTE) {
418
+ if (context.limit !== null) {
419
+ sql += ` LIMIT $${context.addParam(context.limit)}`;
420
+ }
421
+ if (context.cursorId === null && (context.offsetValue > 0 || context.limit !== null)) {
422
+ sql += ` OFFSET $${context.addParam(context.offsetValue)}`;
423
+ }
424
+ }
425
+
426
+ return sql;
427
+ }
428
+
429
+ /**
430
+ * Optimized sorting for direct partition access
431
+ * Queries the partition table directly without going through entity_components for the sort join
432
+ */
433
+ private applySortingOptimized(baseQuery: string, context: QueryContext): string | null {
434
+ if (context.sortOrders.length !== 1) return null;
435
+
436
+ const sortOrder = context.sortOrders[0]!;
437
+ const typeId = ComponentRegistry.getComponentId(sortOrder.component);
438
+ if (!typeId) return null;
439
+
440
+ const partitionTable = ComponentRegistry.getPartitionTableName(typeId);
441
+ if (!partitionTable) return null;
442
+
443
+ const nullsClause = sortOrder.nullsFirst ? 'NULLS FIRST' : 'NULLS LAST';
444
+
445
+ // Optimized query: Direct join to partition table, skip entity_components for sort
446
+ // This is faster because we go directly to the partition table
447
+ let sql = `SELECT base.id FROM (${baseQuery}) AS base
448
+ JOIN ${partitionTable} c ON c.entity_id = base.id
449
+ AND c.type_id = $${context.addParam(typeId)}::text
450
+ AND c.deleted_at IS NULL
451
+ ORDER BY c.data->>'${sortOrder.property}' ${sortOrder.direction} ${nullsClause}`;
452
+
453
+ // Add LIMIT and OFFSET only if not already applied in CTE
454
+ // When pagination is applied at CTE level, skip it here to avoid double pagination
455
+ if (!context.paginationAppliedInCTE) {
456
+ if (context.limit !== null) {
457
+ sql += ` LIMIT $${context.addParam(context.limit)}`;
458
+ }
459
+ // Only add OFFSET when not using cursor-based pagination
460
+ if (context.cursorId === null && (context.offsetValue > 0 || context.limit !== null)) {
461
+ sql += ` OFFSET $${context.addParam(context.offsetValue)}`;
462
+ }
463
+ }
464
+
465
+ return sql;
466
+ }
467
+
468
+ /**
469
+ * Apply component filters using either EXISTS subqueries or LATERAL joins
470
+ */
471
+ private applyComponentFilters(
472
+ context: QueryContext,
473
+ componentIds: string[],
474
+ useCTE: boolean,
475
+ useLateralJoins: boolean,
476
+ lateralJoins: string[],
477
+ lateralConditions: string[],
478
+ sql: string,
479
+ componentParamIndices: Map<string, number>
480
+ ): string {
481
+ for (const [compId, filters] of context.componentFilters) {
482
+ for (const filter of filters) {
483
+ let condition: string;
484
+
485
+ // Check for custom filter builder first
486
+ if (FilterBuilderRegistry.has(filter.operator)) {
487
+ // Validate filter if validator is provided
488
+ const options = FilterBuilderRegistry.getOptions(filter.operator);
489
+ if (options?.validate && !options.validate(filter)) {
490
+ throw new Error(`Invalid filter value for operator '${filter.operator}': ${JSON.stringify(filter.value)}`);
491
+ }
492
+
493
+ const customBuilder = FilterBuilderRegistry.get(filter.operator)!;
494
+ const result = customBuilder(filter, "c", context);
495
+ condition = result.sql;
496
+ // Note: custom builder is responsible for adding parameters via context.addParam()
497
+ } else {
498
+ // Default filter logic
499
+ // Validate filter value to prevent PostgreSQL UUID parsing errors
500
+ if (filter.value === '' || (typeof filter.value === 'string' && filter.value.trim() === '')) {
501
+ throw new Error(`Filter value for field "${filter.field}" is an empty string. This would cause PostgreSQL UUID parsing errors.`);
502
+ }
503
+
504
+ // Check if value looks like a UUID (case-insensitive, with or without hyphens)
505
+ const valueStr = String(filter.value);
506
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(valueStr);
507
+
508
+ // Debug logging
509
+ // console.log('[ComponentInclusionNode] Filter:', {
510
+ // field: filter.field,
511
+ // operator: filter.operator,
512
+ // value: filter.value,
513
+ // valueStr,
514
+ // isUUID
515
+ // });
516
+
517
+ // Build JSON path for nested fields (e.g., "device.unique_id" -> "c.data->'device'->>'unique_id'")
518
+ let jsonPath: string;
519
+ if (filter.field.includes('.')) {
520
+ const parts = filter.field.split('.');
521
+ const lastPart = parts.pop()!;
522
+ const nestedPath = parts.map(p => `'${p}'`).join('->');
523
+ jsonPath = `c.data->${nestedPath}->>'${lastPart}'`;
524
+ } else {
525
+ jsonPath = `c.data->>'${filter.field}'`;
526
+ }
527
+
528
+ if (isUUID && filter.operator === '=') {
529
+ // UUID equality comparison - only cast the parameter, compare as text
530
+ // This allows matching UUID parameter against both UUID and text fields
531
+ condition = `${jsonPath} = $${context.addParam(filter.value)}`;
532
+ } else if (filter.operator === 'LIKE' || filter.operator === 'NOT LIKE' || filter.operator === 'ILIKE') {
533
+ // String LIKE/ILIKE comparison - no casting
534
+ condition = `${jsonPath} ${filter.operator} $${context.addParam(filter.value)}`;
535
+ } else if (filter.operator === 'IN' || filter.operator === 'NOT IN') {
536
+ // IN/NOT IN comparison - handle arrays properly
537
+ if (Array.isArray(filter.value)) {
538
+ const placeholders = Array.from({length: filter.value.length}, (_, i) => `$${context.addParam(filter.value[i])}`).join(', ');
539
+ condition = `${jsonPath} ${filter.operator} (${placeholders})`;
540
+ } else {
541
+ throw new Error(`${filter.operator} operator requires an array of values`);
542
+ }
543
+ } else if (typeof filter.value === 'number') {
544
+ // Only treat as numeric if the value is actually a number type, not a string
545
+ condition = `(${jsonPath})::numeric ${filter.operator} $${context.addParam(filter.value)}::numeric`;
546
+ } else if (typeof filter.value === 'boolean') {
547
+ // Boolean comparison - cast JSON text to boolean
548
+ condition = `(${jsonPath})::boolean ${filter.operator} $${context.addParam(filter.value)}`;
549
+ } else {
550
+ // Default: text comparison without casting
551
+ condition = `${jsonPath} ${filter.operator} $${context.addParam(filter.value)}`;
552
+ }
553
+
554
+ // console.log('[ComponentInclusionNode] Condition:', condition);
555
+ }
556
+
557
+ const tableAlias = useCTE ? context.cteName : "ec";
558
+ const whereKeyword = sql.includes('WHERE') ? 'AND' : 'WHERE';
559
+
560
+ if (useLateralJoins) {
561
+ // Use LATERAL join approach
562
+ // Create a short, unique alias (PostgreSQL has 63 char limit)
563
+ // Use first 8 chars of component ID + field name + index
564
+ const compIdShort = compId.substring(0, 8);
565
+ const fieldShort = filter.field.replace(/\./g, '_').substring(0, 20);
566
+ const lateralAlias = `lat_${compIdShort}_${fieldShort}_${lateralJoins.length}`;
567
+
568
+ const componentTableName = this.getComponentTableName(compId);
569
+ const useDirectPartition = shouldUseDirectPartition() && componentTableName !== 'components';
570
+
571
+ if (useDirectPartition) {
572
+ // Direct partition access - query partition table directly by entity_id
573
+ lateralJoins.push(
574
+ `CROSS JOIN LATERAL (
575
+ SELECT 1 FROM ${componentTableName} c
576
+ WHERE c.entity_id = ${tableAlias}.entity_id
577
+ AND c.type_id = $${componentParamIndices.has(compId) ? componentParamIndices.get(compId) : context.addParam(compId)}::text
578
+ AND ${condition}
579
+ AND c.deleted_at IS NULL
580
+ LIMIT 1
581
+ ) AS ${lateralAlias}`
582
+ );
583
+ } else {
584
+ // Use entity_components junction table
585
+ lateralJoins.push(
586
+ `CROSS JOIN LATERAL (
587
+ SELECT 1 FROM entity_components ec_f
588
+ JOIN ${componentTableName} c ON ec_f.component_id = c.id
589
+ WHERE ec_f.entity_id = ${tableAlias}.entity_id
590
+ AND ec_f.type_id = $${componentParamIndices.has(compId) ? componentParamIndices.get(compId) : context.addParam(compId)}::text
591
+ AND ${condition}
592
+ AND ec_f.deleted_at IS NULL
593
+ AND c.deleted_at IS NULL
594
+ LIMIT 1
595
+ ) AS ${lateralAlias}`
596
+ );
597
+ }
598
+ lateralConditions.push(`${lateralAlias} IS NOT NULL`);
599
+ } else {
600
+ // Use traditional EXISTS subquery
601
+ const componentTableName = this.getComponentTableName(compId);
602
+ const useDirectPartition = shouldUseDirectPartition() && componentTableName !== 'components';
603
+
604
+ if (useDirectPartition) {
605
+ // Direct partition access - query partition table directly by entity_id
606
+ sql += ` ${whereKeyword} EXISTS (
607
+ SELECT 1 FROM ${componentTableName} c
608
+ WHERE c.entity_id = ${tableAlias}.entity_id
609
+ AND c.type_id = $${componentParamIndices.has(compId) ? componentParamIndices.get(compId) : context.addParam(compId)}::text
610
+ AND ${condition}
611
+ AND c.deleted_at IS NULL
612
+ )`;
613
+ } else {
614
+ // Use entity_components junction table
615
+ sql += ` ${whereKeyword} EXISTS (
616
+ SELECT 1 FROM entity_components ec_f
617
+ JOIN ${componentTableName} c ON ec_f.component_id = c.id
618
+ WHERE ec_f.entity_id = ${tableAlias}.entity_id
619
+ AND ec_f.type_id = $${componentParamIndices.has(compId) ? componentParamIndices.get(compId) : context.addParam(compId)}::text
620
+ AND ${condition}
621
+ AND ec_f.deleted_at IS NULL
622
+ AND c.deleted_at IS NULL
623
+ )`;
624
+ }
625
+ }
626
+ }
627
+ }
628
+
629
+ // If using LATERAL joins, add them to the FROM clause and conditions to WHERE
630
+ if (useLateralJoins && lateralJoins.length > 0) {
631
+ // Add LATERAL conditions to WHERE clause FIRST (before inserting LATERAL joins)
632
+ let whereClause = '';
633
+ if (lateralConditions.length > 0) {
634
+ const conditionsString = lateralConditions.join(' AND ');
635
+
636
+ // Find ORDER BY or GROUP BY to determine WHERE insertion point
637
+ const orderByMatch = sql.match(/\s+(ORDER\s+BY)/i);
638
+ const groupByMatch = sql.match(/\s+(GROUP\s+BY)/i);
639
+
640
+ let insertIndex = -1;
641
+ if (orderByMatch) {
642
+ insertIndex = orderByMatch.index!;
643
+ } else if (groupByMatch) {
644
+ insertIndex = groupByMatch.index!;
645
+ }
646
+
647
+ // Check if WHERE already exists in the query (before ORDER BY/GROUP BY)
648
+ const beforeClause = insertIndex !== -1 ? sql.substring(0, insertIndex) : sql;
649
+ const hasWhere = beforeClause.includes(' WHERE ');
650
+ const whereKeyword = hasWhere ? ' AND' : ' WHERE';
651
+ whereClause = `${whereKeyword} ${conditionsString}`;
652
+
653
+ if (insertIndex !== -1) {
654
+ // Insert before ORDER BY or GROUP BY
655
+ sql = sql.substring(0, insertIndex) + whereClause + sql.substring(insertIndex);
656
+ } else {
657
+ // No ORDER BY or GROUP BY, append at end
658
+ sql += whereClause;
659
+ }
660
+ }
661
+
662
+ // Now find the FROM clause and add LATERAL joins after the table name
663
+ const fromIndex = sql.indexOf(' FROM ');
664
+ if (fromIndex !== -1) {
665
+ const afterFromStart = fromIndex + 6; // Position after "FROM "
666
+ const afterFromPart = sql.substring(afterFromStart);
667
+
668
+ // Find the end of the table name/alias (before WHERE, ORDER BY, or GROUP BY)
669
+ let tableEndIndex = afterFromPart.search(/\s+(WHERE|AND|ORDER\s+BY|GROUP\s+BY)/i);
670
+ if (tableEndIndex === -1) {
671
+ tableEndIndex = afterFromPart.length;
672
+ }
673
+
674
+ const tableName = afterFromPart.substring(0, tableEndIndex).trim();
675
+ const restOfQuery = afterFromPart.substring(tableEndIndex);
676
+
677
+ const beforeFrom = sql.substring(0, afterFromStart);
678
+ const lateralSql = lateralJoins.join(' ');
679
+ sql = beforeFrom + tableName + ' ' + lateralSql + restOfQuery;
680
+ }
681
+ }
682
+
683
+ return sql;
684
+ }
685
+
686
+ public getNodeType(): string {
687
+ return "ComponentInclusionNode";
688
+ }
689
+ }