dsl-to-sql 1.0.0-fabric-1p-development.1

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 (225) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/.turbo/turbo-check-types.log +4 -0
  3. package/.turbo/turbo-lint.log +126 -0
  4. package/README.md +45 -0
  5. package/coverage/base.css +224 -0
  6. package/coverage/block-navigation.js +87 -0
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/index.html +236 -0
  9. package/coverage/prettify.css +1 -0
  10. package/coverage/prettify.js +2 -0
  11. package/coverage/sort-arrow-sprite.png +0 -0
  12. package/coverage/sorter.js +210 -0
  13. package/coverage/src/constants.ts.html +826 -0
  14. package/coverage/src/database_types.ts.html +136 -0
  15. package/coverage/src/epm-query-builder/EpmQueryBuilder.ts.html +166 -0
  16. package/coverage/src/epm-query-builder/base/BaseAdvancedAggregations.ts.html +568 -0
  17. package/coverage/src/epm-query-builder/base/BaseAnalyticalFunctions.ts.html +694 -0
  18. package/coverage/src/epm-query-builder/base/BaseCTEGenerator.ts.html +1459 -0
  19. package/coverage/src/epm-query-builder/base/BaseCountQueryBuilder.ts.html +400 -0
  20. package/coverage/src/epm-query-builder/base/BaseMeasureBuilder.ts.html +295 -0
  21. package/coverage/src/epm-query-builder/base/BaseOrderBuilder.ts.html +670 -0
  22. package/coverage/src/epm-query-builder/base/BasePaginationBuilder.ts.html +364 -0
  23. package/coverage/src/epm-query-builder/base/BaseQueryBuilder.ts.html +238 -0
  24. package/coverage/src/epm-query-builder/base/BaseRollupBuilder.ts.html +532 -0
  25. package/coverage/src/epm-query-builder/base/BaseSqlBuilder.ts.html +601 -0
  26. package/coverage/src/epm-query-builder/base/BaseSuperFilterBuilder.ts.html +1966 -0
  27. package/coverage/src/epm-query-builder/base/BaseUtilities.ts.html +1798 -0
  28. package/coverage/src/epm-query-builder/base/ColumnRefUtils.ts.html +211 -0
  29. package/coverage/src/epm-query-builder/base/RelationshipResolver.ts.html +706 -0
  30. package/coverage/src/epm-query-builder/base/SharedFilterBuilder.ts.html +1717 -0
  31. package/coverage/src/epm-query-builder/base/index.html +326 -0
  32. package/coverage/src/epm-query-builder/constants/Aggregations.ts.html +133 -0
  33. package/coverage/src/epm-query-builder/constants/Database.ts.html +103 -0
  34. package/coverage/src/epm-query-builder/constants/Source.ts.html +106 -0
  35. package/coverage/src/epm-query-builder/constants/index.html +146 -0
  36. package/coverage/src/epm-query-builder/dialects/duckdb/DuckDbAdvancedAggregations.ts.html +286 -0
  37. package/coverage/src/epm-query-builder/dialects/duckdb/DuckDbJoinBuilder.ts.html +280 -0
  38. package/coverage/src/epm-query-builder/dialects/duckdb/DuckDbMeasureBuilder.ts.html +1924 -0
  39. package/coverage/src/epm-query-builder/dialects/duckdb/DuckDbOrderBuilder.ts.html +769 -0
  40. package/coverage/src/epm-query-builder/dialects/duckdb/DuckDbPaginationBuilder.ts.html +643 -0
  41. package/coverage/src/epm-query-builder/dialects/duckdb/DuckDbQueryBuilder.ts.html +2644 -0
  42. package/coverage/src/epm-query-builder/dialects/duckdb/DuckDbRollupBuilder.ts.html +478 -0
  43. package/coverage/src/epm-query-builder/dialects/duckdb/DuckDbSuperFilterBuilder.ts.html +1195 -0
  44. package/coverage/src/epm-query-builder/dialects/duckdb/index.html +221 -0
  45. package/coverage/src/epm-query-builder/errors/QueryBuilderErrors.ts.html +280 -0
  46. package/coverage/src/epm-query-builder/errors/index.html +116 -0
  47. package/coverage/src/epm-query-builder/index.html +116 -0
  48. package/coverage/src/epm-query-builder/interfaces/IDatabaseQueryBuilder.ts.html +100 -0
  49. package/coverage/src/epm-query-builder/interfaces/index.html +131 -0
  50. package/coverage/src/epm-query-builder/interfaces/index.ts.html +88 -0
  51. package/coverage/src/epm-query-builder/utils/format.ts.html +151 -0
  52. package/coverage/src/epm-query-builder/utils/index.html +146 -0
  53. package/coverage/src/epm-query-builder/utils/sql.ts.html +247 -0
  54. package/coverage/src/epm-query-builder/utils/validation.ts.html +124 -0
  55. package/coverage/src/epm-query-builder/validation/QueryOptionsValidator.ts.html +631 -0
  56. package/coverage/src/epm-query-builder/validation/SqlQueryValidator.ts.html +475 -0
  57. package/coverage/src/epm-query-builder/validation/index.html +131 -0
  58. package/coverage/src/filters/FilterConditionBuilder.ts.html +427 -0
  59. package/coverage/src/filters/filter-types.ts.html +484 -0
  60. package/coverage/src/filters/index.html +131 -0
  61. package/coverage/src/index.html +176 -0
  62. package/coverage/src/index.ts.html +103 -0
  63. package/coverage/src/js-lib/JsToSqlParser.ts.html +736 -0
  64. package/coverage/src/js-lib/ParseContext.ts.html +532 -0
  65. package/coverage/src/js-lib/db/azuresql/AzureSqlCallExpressionVisitor.ts.html +196 -0
  66. package/coverage/src/js-lib/db/azuresql/index.html +116 -0
  67. package/coverage/src/js-lib/db/base/ArrayExpressionVisitor.ts.html +133 -0
  68. package/coverage/src/js-lib/db/base/AssignmentExpressionVisitor.ts.html +187 -0
  69. package/coverage/src/js-lib/db/base/BinaryExpressionVisitor.ts.html +223 -0
  70. package/coverage/src/js-lib/db/base/CallExpressionVisitor.ts.html +5479 -0
  71. package/coverage/src/js-lib/db/base/IdentifierVisitor.ts.html +283 -0
  72. package/coverage/src/js-lib/db/base/LiteralVisitor.ts.html +199 -0
  73. package/coverage/src/js-lib/db/base/MemberExpressionVisitor.ts.html +193 -0
  74. package/coverage/src/js-lib/db/base/ProgramVisitor.ts.html +139 -0
  75. package/coverage/src/js-lib/db/base/UnaryExpressionVisitor.ts.html +181 -0
  76. package/coverage/src/js-lib/db/base/VisitorInterface.ts.html +103 -0
  77. package/coverage/src/js-lib/db/base/index.html +251 -0
  78. package/coverage/src/js-lib/db/bigquery/BigQueryCallExpressionVisitor.ts.html +1747 -0
  79. package/coverage/src/js-lib/db/bigquery/index.html +116 -0
  80. package/coverage/src/js-lib/db/commonTransforms.ts.html +2074 -0
  81. package/coverage/src/js-lib/db/databricks/DatabricksCallExpressionVisitor.ts.html +1303 -0
  82. package/coverage/src/js-lib/db/databricks/index.html +116 -0
  83. package/coverage/src/js-lib/db/fabricsql/FabricSqlCallExpressionVisitor.ts.html +196 -0
  84. package/coverage/src/js-lib/db/fabricsql/index.html +116 -0
  85. package/coverage/src/js-lib/db/fabricwarehouse/FabricWarehouseCallExpressionVisitor.ts.html +292 -0
  86. package/coverage/src/js-lib/db/fabricwarehouse/index.html +116 -0
  87. package/coverage/src/js-lib/db/index.html +116 -0
  88. package/coverage/src/js-lib/db/postgresql/PostgreSqlCallExpressionVisitor.ts.html +985 -0
  89. package/coverage/src/js-lib/db/postgresql/index.html +116 -0
  90. package/coverage/src/js-lib/db/redshift/RedshiftCallExpressionVisitor.ts.html +685 -0
  91. package/coverage/src/js-lib/db/redshift/index.html +116 -0
  92. package/coverage/src/js-lib/db/sample/SampleCallExpressionVisitor.ts.html +196 -0
  93. package/coverage/src/js-lib/db/sample/index.html +116 -0
  94. package/coverage/src/js-lib/db/snowflake/SnowflakeCallExpressionVisitor.ts.html +1447 -0
  95. package/coverage/src/js-lib/db/snowflake/index.html +116 -0
  96. package/coverage/src/js-lib/db/validator/FormulaValidator.ts.html +4162 -0
  97. package/coverage/src/js-lib/db/validator/index.html +116 -0
  98. package/coverage/src/js-lib/index.html +131 -0
  99. package/coverage/src/js-lib/objects/BaseObject.ts.html +169 -0
  100. package/coverage/src/js-lib/objects/DateObject.ts.html +169 -0
  101. package/coverage/src/js-lib/objects/PctObject.ts.html +178 -0
  102. package/coverage/src/js-lib/objects/index.html +146 -0
  103. package/coverage/src/query-builder/PaginationBuilder.ts.html +142 -0
  104. package/coverage/src/query-builder/QueryBuilder.ts.html +3118 -0
  105. package/coverage/src/query-builder/SuperFilterBuilder.ts.html +1969 -0
  106. package/coverage/src/query-builder/index.html +146 -0
  107. package/coverage/src/runtime_var.ts.html +109 -0
  108. package/coverage/src/sql-lib/binary_expr.ts.html +133 -0
  109. package/coverage/src/sql-lib/case.ts.html +133 -0
  110. package/coverage/src/sql-lib/column.ts.html +139 -0
  111. package/coverage/src/sql-lib/else.ts.html +124 -0
  112. package/coverage/src/sql-lib/function.ts.html +112 -0
  113. package/coverage/src/sql-lib/index.html +251 -0
  114. package/coverage/src/sql-lib/join.ts.html +127 -0
  115. package/coverage/src/sql-lib/literal.ts.html +130 -0
  116. package/coverage/src/sql-lib/select.ts.html +547 -0
  117. package/coverage/src/sql-lib/unary_expr.ts.html +112 -0
  118. package/coverage/src/sql-lib/when.ts.html +130 -0
  119. package/coverage/src/sql_query_gen.ts.html +535 -0
  120. package/coverage/src/superFilter/DateFilterFactory.ts.html +625 -0
  121. package/coverage/src/superFilter/dateFunction.ts.html +193 -0
  122. package/coverage/src/superFilter/index.html +131 -0
  123. package/coverage/src/utils.ts.html +571 -0
  124. package/dist/index.cjs +8440 -0
  125. package/dist/index.d.cts +927 -0
  126. package/dist/index.d.cts.map +1 -0
  127. package/dist/index.d.ts +927 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +8387 -0
  130. package/dist/index.js.map +1 -0
  131. package/eslint.config.js +4 -0
  132. package/jest.config.ts +44 -0
  133. package/package.json +45 -0
  134. package/src/constants.ts +247 -0
  135. package/src/epm-query-builder/EpmQueryBuilder.ts +27 -0
  136. package/src/epm-query-builder/base/BaseAdvancedAggregations.ts +161 -0
  137. package/src/epm-query-builder/base/BaseAnalyticalFunctions.ts +203 -0
  138. package/src/epm-query-builder/base/BaseCTEGenerator.ts +458 -0
  139. package/src/epm-query-builder/base/BaseCountQueryBuilder.ts +105 -0
  140. package/src/epm-query-builder/base/BaseMeasureBuilder.ts +87 -0
  141. package/src/epm-query-builder/base/BaseOrderBuilder.ts +195 -0
  142. package/src/epm-query-builder/base/BasePaginationBuilder.ts +93 -0
  143. package/src/epm-query-builder/base/BaseQueryBuilder.ts +51 -0
  144. package/src/epm-query-builder/base/BaseRollupBuilder.ts +149 -0
  145. package/src/epm-query-builder/base/BaseSqlBuilder.ts +172 -0
  146. package/src/epm-query-builder/base/BaseSuperFilterBuilder.ts +627 -0
  147. package/src/epm-query-builder/base/BaseUtilities.ts +571 -0
  148. package/src/epm-query-builder/base/ColumnRefUtils.ts +42 -0
  149. package/src/epm-query-builder/base/RelationshipResolver.ts +207 -0
  150. package/src/epm-query-builder/base/SharedFilterBuilder.ts +544 -0
  151. package/src/epm-query-builder/constants/Aggregations.ts +16 -0
  152. package/src/epm-query-builder/constants/Database.ts +6 -0
  153. package/src/epm-query-builder/constants/Source.ts +7 -0
  154. package/src/epm-query-builder/dialects/duckdb/DuckDbAdvancedAggregations.ts +67 -0
  155. package/src/epm-query-builder/dialects/duckdb/DuckDbJoinBuilder.ts +65 -0
  156. package/src/epm-query-builder/dialects/duckdb/DuckDbMeasureBuilder.ts +626 -0
  157. package/src/epm-query-builder/dialects/duckdb/DuckDbOrderBuilder.ts +228 -0
  158. package/src/epm-query-builder/dialects/duckdb/DuckDbPaginationBuilder.ts +186 -0
  159. package/src/epm-query-builder/dialects/duckdb/DuckDbQueryBuilder.ts +853 -0
  160. package/src/epm-query-builder/dialects/duckdb/DuckDbRollupBuilder.ts +131 -0
  161. package/src/epm-query-builder/dialects/duckdb/DuckDbSuperFilterBuilder.ts +370 -0
  162. package/src/epm-query-builder/errors/QueryBuilderErrors.ts +65 -0
  163. package/src/epm-query-builder/interfaces/IDatabaseQueryBuilder.ts +5 -0
  164. package/src/epm-query-builder/interfaces/index.ts +1 -0
  165. package/src/epm-query-builder/types/query-builder-types.d.ts +289 -0
  166. package/src/epm-query-builder/utils/format.ts +22 -0
  167. package/src/epm-query-builder/utils/sql.ts +54 -0
  168. package/src/epm-query-builder/utils/validation.ts +13 -0
  169. package/src/epm-query-builder/validation/QueryOptionsValidator.ts +182 -0
  170. package/src/epm-query-builder/validation/SqlQueryValidator.ts +130 -0
  171. package/src/filters/FilterConditionBuilder.ts +114 -0
  172. package/src/filters/filter-types.ts +133 -0
  173. package/src/index.ts +10 -0
  174. package/src/js-lib/JsToSqlParser.ts +217 -0
  175. package/src/js-lib/ParseContext.ts +149 -0
  176. package/src/js-lib/db/base/ArrayExpressionVisitor.ts +16 -0
  177. package/src/js-lib/db/base/AssignmentExpressionVisitor.ts +34 -0
  178. package/src/js-lib/db/base/BinaryExpressionVisitor.ts +46 -0
  179. package/src/js-lib/db/base/CallExpressionVisitor.ts +1798 -0
  180. package/src/js-lib/db/base/IdentifierVisitor.ts +66 -0
  181. package/src/js-lib/db/base/LiteralVisitor.ts +38 -0
  182. package/src/js-lib/db/base/MemberExpressionVisitor.ts +36 -0
  183. package/src/js-lib/db/base/ProgramVisitor.ts +18 -0
  184. package/src/js-lib/db/base/UnaryExpressionVisitor.ts +32 -0
  185. package/src/js-lib/db/base/VisitorInterface.ts +6 -0
  186. package/src/js-lib/db/validator/FormulaValidator.ts +1235 -0
  187. package/src/js-lib/objects/BaseObject.ts +28 -0
  188. package/src/js-lib/objects/DateObject.ts +28 -0
  189. package/src/js-lib/objects/PctObject.ts +31 -0
  190. package/src/query-builder/PaginationBuilder.ts +19 -0
  191. package/src/query-builder/QueryBuilder.ts +1035 -0
  192. package/src/query-builder/SuperFilterBuilder.ts +628 -0
  193. package/src/runtime_var.ts +8 -0
  194. package/src/sql-lib/binary_expr.ts +16 -0
  195. package/src/sql-lib/case.ts +16 -0
  196. package/src/sql-lib/column.ts +18 -0
  197. package/src/sql-lib/else.ts +13 -0
  198. package/src/sql-lib/function.ts +9 -0
  199. package/src/sql-lib/join.ts +14 -0
  200. package/src/sql-lib/literal.ts +15 -0
  201. package/src/sql-lib/select.ts +154 -0
  202. package/src/sql-lib/unary_expr.ts +9 -0
  203. package/src/sql-lib/when.ts +15 -0
  204. package/src/sql-types.d.ts +565 -0
  205. package/src/sql_query_gen.ts +150 -0
  206. package/src/superFilter/DateFilterFactory.ts +180 -0
  207. package/src/superFilter/dateFunction.ts +36 -0
  208. package/src/utils.ts +354 -0
  209. package/test-output/report/junit.xml +329 -0
  210. package/tests/JsToSqlParser.test.ts +163 -0
  211. package/tests/QueryBuilder.test.ts +1320 -0
  212. package/tests/js-lib/CallExpressionVisitor.test.ts +820 -0
  213. package/tests/mocks/MockQueryResolver.ts +14 -0
  214. package/tests/sanity.test.ts +146 -0
  215. package/tests/sql-lib/binary_expr.test.ts +75 -0
  216. package/tests/sql-lib/case.test.ts +117 -0
  217. package/tests/sql-lib/column.test.ts +87 -0
  218. package/tests/sql-lib/else.test.ts +56 -0
  219. package/tests/sql-lib/function.test.ts +96 -0
  220. package/tests/sql-lib/literal.test.ts +75 -0
  221. package/tests/sql-lib/select.test.ts +245 -0
  222. package/tests/sql-lib/unary_expr.test.ts +32 -0
  223. package/tests/utils.test.ts +13 -0
  224. package/tsconfig.json +24 -0
  225. package/tsdown.config.ts +23 -0
@@ -0,0 +1,1035 @@
1
+ import {
2
+ IMDMColumnConfigWithParsedMeta,
3
+ MDM_COLUMN_TYPE,
4
+ ResolvedExpressionType,
5
+ CteExpression,
6
+ } from '../js-lib/ParseContext';
7
+ import {
8
+ FilterConfig,
9
+ JoinExpressions,
10
+ Rule,
11
+ RuleGroup,
12
+ ISuperFilterConfig,
13
+ IGroupFilter,
14
+ IManualFilterConfig,
15
+ } from '../filters/filter-types';
16
+ import { RUNTIME_TABLE_NAME } from '../runtime_var';
17
+ import { PaginationBuilder, createPaginationBuilder } from './PaginationBuilder';
18
+ import { From, RawSqlExpression, Select } from '../sql-types';
19
+ import { SelectBuilder } from '../sql-lib/select';
20
+ import { toColumn } from '../sql-lib/column';
21
+ import { processSelect } from '../sql_query_gen';
22
+ import {
23
+ getCurrentDate,
24
+ getCurrentDateTime,
25
+ getEntityNameFormatter,
26
+ getFormattedTableName,
27
+ getJoinExpressions,
28
+ resolveRuntimeVariables,
29
+ stripWhereClause,
30
+ } from '../utils';
31
+ import { SuperFilterBuilder } from './SuperFilterBuilder';
32
+
33
+ export type SortConfig = { column: string; order: 'ASC' | 'DESC' }[];
34
+
35
+ export type PaginationConfig = { skip: number; take: number };
36
+
37
+ export type FilterResolverParams = {
38
+ filterConfig: FilterConfig;
39
+ schema?: IColumnSchemaItem[];
40
+ };
41
+
42
+ export interface QueryResolver {
43
+ filterResolver(params: FilterResolverParams): string;
44
+ }
45
+
46
+ export type RuntimeVariables = {
47
+ $RUNTIME_LOGGEDIN_NAME: string;
48
+ $RUNTIME_LOGGEDIN_EMAIL: string;
49
+ $RUNTIME_CURRENT_DATE?: string;
50
+ $RUNTIME_CURRENT_DATETIME?: string;
51
+ };
52
+
53
+ export type QueryBuilderOptions = {
54
+ selectedColumns: string[];
55
+ databaseDetails: Record<string, any>;
56
+ primaryKeyColumns: string[];
57
+ filterConfig?: FilterConfig;
58
+ pagination?: PaginationConfig;
59
+ sortConfig?: SortConfig;
60
+ searchExpression?: string;
61
+ distinct?: boolean;
62
+ columnConfigMap: Record<string, IMDMColumnConfigWithParsedMeta>;
63
+ queryResolver: QueryResolver; // instance of BaseDB in database-service-link
64
+ runtimeVariables: RuntimeVariables;
65
+ schema?: IColumnSchemaItem[];
66
+ includeFinalCTESelectQuery?: boolean;
67
+ finalResponseAsObject?: boolean;
68
+ superFilters?: ISuperFilterConfig;
69
+ skipCountForRankingFilter?: boolean;
70
+ joinClauses?: JoinExpressions;
71
+ manualFilterOptions?: IManualFilterConfig[];
72
+ enableLastSelectCTEWrap?: boolean;
73
+ };
74
+
75
+ type BuildState = {
76
+ pendingFilterConfig: FilterConfig;
77
+ pendingSortConfig: SortConfig;
78
+ isSortPending: boolean;
79
+ isPaginationPending: boolean;
80
+ isSearchPending: boolean;
81
+ hasFormulaColumns: boolean;
82
+ isSuperFilterPending: boolean;
83
+ };
84
+
85
+ type BuildOptions = Partial<Omit<QueryBuilderOptions, 'tableName' | 'queryResolver' | 'schema'>>;
86
+
87
+ type MainCteExpression = {
88
+ type: ResolvedExpressionType.CTE;
89
+ cteName: string;
90
+ value: Select;
91
+ };
92
+
93
+ export type FinalQueryResult = {
94
+ finalQuery: string;
95
+ finalCteName?: string; // For group by feat need to pass cte name
96
+ };
97
+
98
+ type CalculationOrderResult = {
99
+ nativeColumns: Set<string>;
100
+ formulaColumnsByDependencyLevel: IMDMColumnConfigWithParsedMeta[][];
101
+ };
102
+
103
+ export enum DEFAULT_VALUE_TYPE {
104
+ MANUAL = 'MANUAL',
105
+ DERIVED = 'DERIVED',
106
+ NONE = 'NONE',
107
+ }
108
+
109
+ type IColumnSchemaItem = {
110
+ name: string;
111
+ type: string;
112
+ defaultValue?: string | number;
113
+ isNullable?: boolean;
114
+ isIdentity?: boolean;
115
+ };
116
+
117
+ export const MAX_STRING_TYPE = 'nvarchar(max)';
118
+
119
+ /**
120
+ * Steps:
121
+ * 1. Separate out filters for native columns and formula columns
122
+ * 2. Apply filters for native columns in the main CTE. If Native columns have sorting config then -
123
+ * - If SortConfig exists for Formula columns with cte, do not apply sorting
124
+ * - If SortConfig exists for Formula columns without cte, apply sorting
125
+ * - If SortConfig does not exist for Formula columns, apply sorting
126
+ * 3. Apply filters for formula columns in the CTE followed by the column specific CTEs due to alias not supported in where clause
127
+ * 4. Apply SortConfig and pagination in the CTE with last filter
128
+ * 5. Perform final projection and apply SortConfig and Pagination, if still exists
129
+ */
130
+ export class QueryBuilder {
131
+ private readonly paginationBuilder: PaginationBuilder;
132
+ private readonly options: QueryBuilderOptions;
133
+
134
+ constructor(options: QueryBuilderOptions) {
135
+ this.options = options;
136
+ this.paginationBuilder = createPaginationBuilder();
137
+ }
138
+
139
+ build(options: BuildOptions = {}): FinalQueryResult {
140
+ const combinedOptions = { ...this.options, ...options };
141
+ const impl = new QueryBuilderImpl(combinedOptions, this.paginationBuilder);
142
+ return impl.build();
143
+ }
144
+
145
+ buildCountQuery(options: BuildOptions = {}): FinalQueryResult {
146
+ const combinedOptions = {
147
+ ...this.options,
148
+ ...options,
149
+ sortConfig: undefined, // not required for count query
150
+ pagination: undefined, // not required for count query
151
+ };
152
+ const impl = new QueryBuilderImpl(combinedOptions, this.paginationBuilder);
153
+ return impl.build(true);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Implementation class for QueryBuilder
159
+ *
160
+ * @private exposed for testing
161
+ */
162
+ export class QueryBuilderImpl {
163
+ private readonly paginationBuilder: PaginationBuilder;
164
+ private readonly options: QueryBuilderOptions;
165
+ private buildState: BuildState;
166
+ private readonly entityNameFormatter: (name: string, type?: string) => string;
167
+
168
+ constructor(options: QueryBuilderOptions, paginationBuilder: PaginationBuilder) {
169
+ this.options = {
170
+ ...options,
171
+ searchExpression: options.searchExpression
172
+ ? stripWhereClause(options.searchExpression)
173
+ : undefined,
174
+ sortConfig: this.getRelevantSortConfig(options.sortConfig, new Set(options.selectedColumns)),
175
+ };
176
+ this.paginationBuilder = paginationBuilder;
177
+ this.buildState = {
178
+ pendingFilterConfig: {
179
+ ...(options.filterConfig ?? { combinator: 'and', not: false }),
180
+ rules: [],
181
+ },
182
+ hasFormulaColumns: false,
183
+ pendingSortConfig: [],
184
+ isSortPending: true,
185
+ isPaginationPending: true,
186
+ isSearchPending: options.searchExpression !== undefined,
187
+ isSuperFilterPending: options.superFilters !== undefined,
188
+ };
189
+ this.entityNameFormatter = getEntityNameFormatter();
190
+ // Set runtime variables with current date/time values
191
+ Object.assign(this.options.runtimeVariables, {
192
+ $RUNTIME_CURRENT_DATE: getCurrentDate(),
193
+ $RUNTIME_CURRENT_DATETIME: getCurrentDateTime(),
194
+ });
195
+ }
196
+
197
+ build(isCountQuery = false): FinalQueryResult {
198
+ const { joinClauses, superFilters } = this.options;
199
+ const { joinRelation = [] } = joinClauses || {};
200
+ const requiredColumns = this.filterRequiredColumns();
201
+ this.addColumnsFromFilterConfig(requiredColumns);
202
+ const { nativeColumns, formulaColumnsByDependencyLevel } =
203
+ this.getCalculationOrder(requiredColumns);
204
+ this.buildState.hasFormulaColumns = formulaColumnsByDependencyLevel.length > 0;
205
+ this.buildState.isSuperFilterPending = superFilters !== undefined;
206
+
207
+ // build main CTE and intermediate CTE for formula columns join relation
208
+ const mainCteFormulas = joinRelation.length > 0 ? [] : formulaColumnsByDependencyLevel;
209
+ const columnExprs = this.retrieveColumnExprsInFront(mainCteFormulas);
210
+ const mainCTE = this.buildMainCTE(nativeColumns, columnExprs);
211
+ const intermediateCTE = this.buildIntermediateCTE(
212
+ mainCTE.cteName,
213
+ formulaColumnsByDependencyLevel,
214
+ );
215
+ return this.buildFinalProjection(mainCTE, intermediateCTE, isCountQuery);
216
+ }
217
+
218
+ /**
219
+ * Returns the sort config with only the columns that are part of the requested columns
220
+ */
221
+ private getRelevantSortConfig(
222
+ sortConfig: SortConfig | undefined,
223
+ requestedColumns: Set<string>,
224
+ ): SortConfig | undefined {
225
+ return sortConfig?.filter((s) => requestedColumns.has(s.column));
226
+ }
227
+
228
+ /**
229
+ * Adds columns used in the filter config to the set of all columns
230
+ */
231
+ private addColumnsFromFilterConfig(requiredColumns: Set<string>): void {
232
+ const { filterConfig } = this.options;
233
+
234
+ if (!filterConfig) return;
235
+
236
+ const traverse = (rule: Rule | RuleGroup) => {
237
+ if ('column' in rule) {
238
+ requiredColumns.add(rule.column);
239
+ } else if ('rules' in rule) {
240
+ rule.rules.forEach(traverse);
241
+ }
242
+ };
243
+ filterConfig.rules.forEach(traverse);
244
+ }
245
+
246
+ private retrieveColumnExprsInFront(
247
+ calcOrderMembers: IMDMColumnConfigWithParsedMeta[][],
248
+ ): IMDMColumnConfigWithParsedMeta[] {
249
+ if (calcOrderMembers.length === 0) return [];
250
+
251
+ let lastColumnExprIndex = -1;
252
+ const firstCalcOrderMembers = calcOrderMembers[0];
253
+ for (let i = 0; i < firstCalcOrderMembers.length; i++) {
254
+ if (
255
+ firstCalcOrderMembers[i].columnMeta.sqlQueryProps?.resolvedExpression.type ===
256
+ ResolvedExpressionType.COLUMN
257
+ ) {
258
+ lastColumnExprIndex = i;
259
+ } else {
260
+ break;
261
+ }
262
+ }
263
+
264
+ if (lastColumnExprIndex === -1) {
265
+ return [];
266
+ }
267
+
268
+ const columnExprsInFront = firstCalcOrderMembers.splice(0, lastColumnExprIndex + 1);
269
+ if (firstCalcOrderMembers.length === 0) {
270
+ calcOrderMembers.shift();
271
+ }
272
+ return columnExprsInFront;
273
+ }
274
+
275
+ private buildMainCTE(
276
+ nativeColumns: Set<string>,
277
+ columnExprs: IMDMColumnConfigWithParsedMeta[],
278
+ ): MainCteExpression {
279
+ const columnsInMainCTE = new Set([...nativeColumns, ...columnExprs.map((c) => c.columnName)]);
280
+ const selectBuilder = new SelectBuilder();
281
+ const { joinClauses } = this.options;
282
+ const { alias } = joinClauses || {};
283
+ const tableAlias = alias ? `${this.entityNameFormatter(alias)}.` : '';
284
+
285
+ const columns = Array.from(nativeColumns).map((c) => {
286
+ const schemaMatch = this.options.schema?.find((s) => s.name === c);
287
+ return toColumn({
288
+ type: 'sql_expr',
289
+ value: `${tableAlias}` + this.entityNameFormatter(c, schemaMatch?.type ?? ''),
290
+ meta: { rawColumnName: c },
291
+ });
292
+ });
293
+
294
+ if (columnExprs.length) {
295
+ columnExprs.forEach((c) => {
296
+ const schemaMatch = this.options.schema?.find((s) => s.name === c.columnName);
297
+ let resolvedExpressionValue =
298
+ c.columnMeta.sqlQueryProps?.resolvedExpression?.value?.join(', ');
299
+ if (resolvedExpressionValue) {
300
+ resolvedExpressionValue = resolveRuntimeVariables(
301
+ resolvedExpressionValue,
302
+ this.options.runtimeVariables,
303
+ );
304
+ }
305
+ columns.push(
306
+ toColumn(
307
+ {
308
+ type: 'sql_expr',
309
+ value: resolvedExpressionValue ?? '',
310
+ meta: { rawColumnName: c.columnName },
311
+ },
312
+ this.entityNameFormatter(c.columnName, schemaMatch?.type ?? ''),
313
+ ),
314
+ );
315
+ });
316
+ }
317
+
318
+ const formattedTableName = getFormattedTableName(this.options.databaseDetails);
319
+ selectBuilder.addColumns(columns);
320
+ selectBuilder.addFrom([
321
+ {
322
+ type: 'from',
323
+ table: `${formattedTableName}${alias ? ` AS ${this.entityNameFormatter(alias)}` : ''}`,
324
+ } as From,
325
+ ]);
326
+
327
+ // Build filter condition
328
+ const where = [];
329
+ const filterCondition = this.getFilterExpression(columnsInMainCTE);
330
+ if (filterCondition) {
331
+ where.push(filterCondition);
332
+ }
333
+
334
+ if (
335
+ this.buildState.isSearchPending &&
336
+ this.buildState.isSuperFilterPending === false &&
337
+ this.buildState.hasFormulaColumns === false
338
+ ) {
339
+ this.buildState.isSearchPending = false;
340
+ where.push(this.options.searchExpression);
341
+ }
342
+ if (where.length) {
343
+ selectBuilder.addWhere({
344
+ type: 'sql_expr',
345
+ value: where.join(' AND '),
346
+ } as RawSqlExpression);
347
+ }
348
+
349
+ // Build sort expression
350
+ const sortExpression = this.getSortExpression(columnsInMainCTE);
351
+ if (sortExpression) {
352
+ selectBuilder.addOrderby([{ type: 'sql_expr', value: sortExpression } as RawSqlExpression]);
353
+ }
354
+
355
+ // Build pagination expression
356
+ const paginationExpression = this.getPaginationExpression();
357
+ if (paginationExpression) {
358
+ selectBuilder.addLimit({
359
+ type: 'sql_expr',
360
+ value: paginationExpression,
361
+ } as RawSqlExpression);
362
+ }
363
+
364
+ return {
365
+ type: ResolvedExpressionType.CTE,
366
+ cteName: 'cte_main',
367
+ value: selectBuilder.build(),
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Returns the filter expression for the selected columns. If column type is formula,
373
+ * stores the rules in pending filter config and process it in the next call to getFilterExpression
374
+ * else processes immediately.
375
+ *
376
+ * Note - Pass empty set to process any pending filter config
377
+ */
378
+ private getFilterExpression(columns: Set<string>): string | undefined {
379
+ const { filterConfig, columnConfigMap, queryResolver, databaseDetails } = this.options;
380
+ if (!filterConfig) {
381
+ return;
382
+ }
383
+
384
+ // extract pending filter config
385
+ const filterConfigToProcess = this.buildState.pendingFilterConfig;
386
+
387
+ // Reset pending filter config
388
+ this.buildState.pendingFilterConfig = {
389
+ rules: [],
390
+ combinator: filterConfig.combinator,
391
+ not: filterConfig.not,
392
+ };
393
+
394
+ if (columns.size) {
395
+ const nativeColumnRules: (Rule | RuleGroup)[] = [];
396
+ const formulaColumnRules: (Rule | RuleGroup)[] = [];
397
+ // Get rules for selected columns from original filter config
398
+ filterConfig.rules.forEach((rule) => {
399
+ const columnName = (rule as Rule).column ?? (rule as RuleGroup).rules[0].column;
400
+ if (columns.has(columnName)) {
401
+ if (columnConfigMap[columnName].columnType === MDM_COLUMN_TYPE.FORMULA) {
402
+ formulaColumnRules.push(rule);
403
+
404
+ const schema = this.options.schema ?? [];
405
+ const exists = schema.some((col) => col.name === columnName);
406
+ if (!exists) {
407
+ schema.push({
408
+ name: columnName,
409
+ type: MAX_STRING_TYPE,
410
+ });
411
+ }
412
+ } else {
413
+ nativeColumnRules.push(rule);
414
+ }
415
+ }
416
+ });
417
+
418
+ // if column type is formula, store the rules in pending filter config else process immediately
419
+ if (formulaColumnRules.length) {
420
+ this.buildState.pendingFilterConfig.rules = formulaColumnRules;
421
+ }
422
+ if (nativeColumnRules.length) {
423
+ filterConfigToProcess.rules = filterConfigToProcess.rules.concat(nativeColumnRules);
424
+ }
425
+ }
426
+
427
+ if (filterConfigToProcess.rules.length) {
428
+ const resolvedFilterQuery = queryResolver.filterResolver({
429
+ filterConfig: filterConfigToProcess,
430
+ schema: this.options.schema,
431
+ });
432
+ return stripWhereClause(resolvedFilterQuery);
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Returns the sort expression if each column in the sort config is part of selected columns
438
+ */
439
+ private getSortExpression(columns: Set<string>): string | undefined {
440
+ const { sortConfig, joinClauses } = this.options;
441
+ const { alias } = joinClauses || {};
442
+ if (!sortConfig || sortConfig.length === 0) {
443
+ this.buildState.isSortPending = false;
444
+ return;
445
+ }
446
+
447
+ if (this.buildState.isSortPending === false) {
448
+ return;
449
+ }
450
+
451
+ const selectedSortColumns = sortConfig.filter((s) => columns.has(s.column));
452
+ this.buildState.pendingSortConfig = (this.buildState.pendingSortConfig ?? []).concat(
453
+ selectedSortColumns,
454
+ );
455
+
456
+ const hasPendingFilters = this.buildState.pendingFilterConfig.rules.length > 0;
457
+ if (
458
+ this.buildState.pendingSortConfig.length === sortConfig.length &&
459
+ !hasPendingFilters &&
460
+ !this.buildState.isSearchPending &&
461
+ !this.buildState.isSuperFilterPending
462
+ ) {
463
+ this.buildState.isSortPending = false;
464
+ return `${sortConfig.map((s) => `${alias && !this.buildState.isSuperFilterPending ? `${this.entityNameFormatter(alias)}.` : ''}${this.entityNameFormatter(s.column)} ${s.order}`).join(', ')}`;
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Returns the pagination expression if the pagination config is set and sort is not pending.
470
+ * It must be called after getSortExpression.
471
+ */
472
+ private getPaginationExpression(): string | undefined {
473
+ const { pagination } = this.options;
474
+ if (!pagination) {
475
+ this.buildState.isPaginationPending = false;
476
+ return;
477
+ }
478
+
479
+ // If pagination is already processed, return
480
+ if (this.buildState.isPaginationPending === false) {
481
+ return;
482
+ }
483
+
484
+ // If sort is not processed, throw error
485
+ if (this.buildState.pendingSortConfig === undefined) {
486
+ throw new Error('getSortExpression() must be called before getPaginationExpression()');
487
+ }
488
+
489
+ // If sort is not complete, return
490
+ if (this.buildState.isSortPending || this.buildState.isSearchPending) {
491
+ this.buildState.isPaginationPending = true;
492
+ return;
493
+ }
494
+
495
+ // If pagination can be completed, return
496
+ this.buildState.isPaginationPending = false;
497
+ return this.paginationBuilder.generateQuery(pagination.skip, pagination.take);
498
+ }
499
+
500
+ /**
501
+ * Builds the intermediate CTEs for the formula columns
502
+ * 1. Replace table name with previous cte name
503
+ * 2. Apply filter condition
504
+ * 3. Apply sort expression, if applicable
505
+ * 4. Apply pagination, if applicable
506
+ * 5. Return CTEs/Column expressions
507
+ */
508
+ private buildIntermediateCTE(
509
+ previousCTEName: string,
510
+ formulaColumnsInCalcOrder: IMDMColumnConfigWithParsedMeta[][],
511
+ ): CteExpression {
512
+ let parentCTEName = previousCTEName;
513
+ const tableNameRegex = new RegExp(`\\${RUNTIME_TABLE_NAME}`, 'g');
514
+
515
+ const cteParts: string[] = [];
516
+ formulaColumnsInCalcOrder.forEach((stageMembers) => {
517
+ const pendingColumns: IMDMColumnConfigWithParsedMeta[] = [];
518
+
519
+ stageMembers.forEach((column) => {
520
+ const {
521
+ columnName,
522
+ columnMeta: { sqlQueryProps },
523
+ } = column;
524
+ const isCteExpression =
525
+ sqlQueryProps?.resolvedExpression?.type === ResolvedExpressionType.CTE;
526
+ const intermediateParts = [];
527
+
528
+ if (isCteExpression) {
529
+ // add staging cte to process any pending filters, sort, pagination
530
+ const stagingCte = this.addStagingCTE(parentCTEName, pendingColumns);
531
+ if (stagingCte) {
532
+ pendingColumns.length = 0;
533
+ parentCTEName = stagingCte!.cteName;
534
+ intermediateParts.push(stagingCte.expr);
535
+ }
536
+
537
+ // process current column cte
538
+ const sqlParts = this.buildSqlQueryParts([columnName]);
539
+ const expr = sqlQueryProps.resolvedExpression as CteExpression;
540
+ let resolvedCte = expr.value.join(', ').replace(tableNameRegex, parentCTEName);
541
+ if (resolvedCte) {
542
+ resolvedCte = resolveRuntimeVariables(resolvedCte, this.options.runtimeVariables);
543
+ }
544
+ parentCTEName = expr.cteName;
545
+ intermediateParts.push(resolvedCte);
546
+ if (sqlParts.length) {
547
+ const cteName = `cte_post_${parentCTEName}`;
548
+ const postCte = `${cteName} AS (SELECT * FROM ${parentCTEName}${sqlParts.length ? ' ' + sqlParts.join(' ') : ''})`;
549
+ intermediateParts.push(postCte);
550
+ parentCTEName = cteName;
551
+ }
552
+ cteParts.push(intermediateParts.join(', '));
553
+ } else {
554
+ pendingColumns.push(column);
555
+ }
556
+ });
557
+
558
+ // add staging cte if there are pending columns at the end of stage.
559
+ if (pendingColumns.length) {
560
+ const stagingCte = this.addStagingCTE(parentCTEName, pendingColumns);
561
+ pendingColumns.length = 0;
562
+ if (stagingCte) {
563
+ cteParts.push(stagingCte.expr);
564
+ parentCTEName = stagingCte.cteName;
565
+ }
566
+ }
567
+ });
568
+
569
+ return { type: ResolvedExpressionType.CTE, cteName: parentCTEName, value: cteParts };
570
+ }
571
+
572
+ private addStagingCTE(
573
+ parentCTEName: string,
574
+ columnConfigs: IMDMColumnConfigWithParsedMeta[],
575
+ ): { cteName: string; expr: string } | undefined {
576
+ const pendingColumnsSet = new Set(columnConfigs.map((config) => config.columnName));
577
+ const sqlParts = this.buildSqlQueryParts(Array.from(pendingColumnsSet));
578
+
579
+ if (sqlParts.length !== 0 || columnConfigs.length !== 0) {
580
+ const cteName = `cte_staging_${parentCTEName}`;
581
+ const columnsToProject = [
582
+ '*',
583
+ ...columnConfigs.map((config) => {
584
+ let resolvedExpressionValue =
585
+ config.columnMeta.sqlQueryProps?.resolvedExpression?.value?.join(', ');
586
+ if (resolvedExpressionValue) {
587
+ resolvedExpressionValue = resolveRuntimeVariables(
588
+ resolvedExpressionValue,
589
+ this.options.runtimeVariables,
590
+ );
591
+ }
592
+ return `${resolvedExpressionValue} AS ${this.entityNameFormatter(config.columnName)}`;
593
+ }),
594
+ ].join(', ');
595
+ const stagingCte = `${cteName} AS (SELECT ${columnsToProject} FROM ${parentCTEName}${sqlParts.length ? ' ' + sqlParts.join(' ') : ''})`;
596
+ return { cteName, expr: stagingCte };
597
+ }
598
+ }
599
+
600
+ private buildFinalProjection(
601
+ mainCTE: MainCteExpression,
602
+ intermediateCTE: CteExpression,
603
+ isCountQuery: boolean = false,
604
+ ): FinalQueryResult {
605
+ const {
606
+ distinct,
607
+ selectedColumns,
608
+ includeFinalCTESelectQuery = true,
609
+ finalResponseAsObject = false,
610
+ joinClauses,
611
+ enableLastSelectCTEWrap = false,
612
+ } = this.options;
613
+ const { joinRelation, alias } = joinClauses || {};
614
+ const mainTableAlias = alias ? `${this.entityNameFormatter(alias)}` : '';
615
+
616
+ // final query is select from main cte and intermediate cte if exists and isFilterPending is false
617
+ const finalSqlParts = this.buildState.isSuperFilterPending ? false : true;
618
+ const sqlParts = this.buildSqlQueryParts([], finalSqlParts);
619
+
620
+ if (
621
+ sqlParts.length === 0 &&
622
+ intermediateCTE.value.length === 0 &&
623
+ !this.buildState.isSuperFilterPending
624
+ ) {
625
+ if (joinRelation?.length) {
626
+ mainCTE.value.from = this.applyJoinExpressionToMainCTE(mainCTE.value);
627
+ }
628
+ if (distinct) {
629
+ this.applyDistinctExpressionToMainCTE(mainCTE);
630
+ }
631
+ const query = isCountQuery
632
+ ? this.applyCountExpressionToMainCTE(mainCTE)
633
+ : processSelect(mainCTE.value);
634
+ return {
635
+ finalQuery: query,
636
+ };
637
+ } else {
638
+ let currentCteName = mainCTE.cteName;
639
+ const cteParts = [`cte_main AS (${processSelect(mainCTE.value)})`];
640
+ if (intermediateCTE.value.length > 0) {
641
+ currentCteName = intermediateCTE.cteName;
642
+ cteParts.push(intermediateCTE.value.join(', '));
643
+ }
644
+ let tableName = currentCteName;
645
+ if (joinRelation?.length) {
646
+ const joinExpr = getJoinExpressions(
647
+ joinClauses as JoinExpressions,
648
+ this.options?.databaseDetails,
649
+ ).join(' ');
650
+ sqlParts.splice(0, 0, joinExpr);
651
+ }
652
+ let column = '*';
653
+ if (distinct) {
654
+ tableName = `${tableName}${mainTableAlias ? ` AS ${mainTableAlias}` : ''}`;
655
+
656
+ column = `DISTINCT ${selectedColumns
657
+ .map((c) => {
658
+ const schemaMatch = this.options.schema?.find((s) => s.name === c);
659
+ return (
660
+ `${mainTableAlias ? `${mainTableAlias}.` : ''}` +
661
+ this.entityNameFormatter(c, schemaMatch?.type ?? '')
662
+ );
663
+ })
664
+ .join(', ')}`;
665
+ }
666
+ if (isCountQuery) {
667
+ if (distinct) {
668
+ const distinctCountCte = this.getDistinctCountCte(currentCteName, sqlParts);
669
+ sqlParts.length = 0;
670
+ tableName = distinctCountCte.cteName;
671
+ cteParts.push(distinctCountCte.value.join(', '));
672
+ column = `COUNT(*) AS row_count`;
673
+ } else {
674
+ if (joinRelation?.length) {
675
+ const primaryKeyColumns = this.options.primaryKeyColumns?.[0];
676
+ if (mainTableAlias && primaryKeyColumns) {
677
+ column = `DISTINCT ${mainTableAlias}.${this.entityNameFormatter(primaryKeyColumns)}`;
678
+ }
679
+ }
680
+ column = `COUNT(${column}) AS row_count`;
681
+ }
682
+ }
683
+ if (!distinct) {
684
+ tableName = `${tableName}${mainTableAlias ? ` AS ${mainTableAlias}` : ''}`;
685
+ }
686
+ let selectedColumn =
687
+ column === '*' && mainTableAlias ? `DISTINCT ${mainTableAlias}.*` : column;
688
+
689
+ if (isCountQuery && this.buildState.isSuperFilterPending) {
690
+ selectedColumn = mainTableAlias && !distinct ? `DISTINCT ${mainTableAlias}.*` : '*';
691
+ }
692
+
693
+ const selectedProjection = `SELECT ${selectedColumn} FROM ${tableName}${sqlParts.length ? ' ' + sqlParts.join(' ') : ''}`;
694
+
695
+ let finalProjection = '';
696
+ if (this.buildState.isSuperFilterPending || enableLastSelectCTEWrap) {
697
+ const { cteDefinition, finalQuery, finalCteName } = this.buildFinalQueryComponents(
698
+ currentCteName,
699
+ selectedProjection,
700
+ isCountQuery,
701
+ distinct,
702
+ );
703
+ currentCteName = finalCteName;
704
+ cteParts.push(cteDefinition);
705
+ finalProjection = finalQuery;
706
+ } else {
707
+ finalProjection = selectedProjection;
708
+ }
709
+ const cteJoins = enableLastSelectCTEWrap
710
+ ? cteParts.join(', ')
711
+ : `WITH ${cteParts.join(', ')}`;
712
+ let finalQuery = cteJoins;
713
+ if (includeFinalCTESelectQuery) {
714
+ finalQuery += ` ${finalProjection}`;
715
+ }
716
+
717
+ if (finalResponseAsObject) {
718
+ return {
719
+ finalQuery: finalQuery,
720
+ finalCteName: currentCteName,
721
+ };
722
+ }
723
+
724
+ return {
725
+ finalQuery,
726
+ };
727
+ }
728
+ }
729
+
730
+ private applyDistinctExpressionToMainCTE(mainCTE: MainCteExpression) {
731
+ const { selectedColumns } = this.options;
732
+ if (mainCTE.value.columns.length > selectedColumns.length) {
733
+ mainCTE.value.columns = mainCTE.value.columns.filter((c) => {
734
+ return selectedColumns.includes(c.expr.meta?.rawColumnName);
735
+ });
736
+ }
737
+ mainCTE.value.distinct = 'DISTINCT';
738
+ }
739
+
740
+ private applyCountExpressionToMainCTE(mainCTE: MainCteExpression): string {
741
+ if (mainCTE.value.distinct) {
742
+ const mainCteResult = `WITH cte_main AS (${processSelect(mainCTE.value)})`;
743
+ return `${mainCteResult} SELECT COUNT(*) AS row_count FROM cte_main`;
744
+ }
745
+
746
+ const columnValue =
747
+ mainCTE.value.columns.length > 1 ? '*' : `${mainCTE.value.columns[0].expr.value}`;
748
+ mainCTE.value.columns[0].expr.value = `COUNT(${columnValue})`;
749
+ mainCTE.value.columns[0].as = `row_count`;
750
+ mainCTE.value.columns.length = 1;
751
+ return processSelect(mainCTE.value);
752
+ }
753
+
754
+ private getDistinctCountCte(parentCTEName: string, sqlParts: string[]): CteExpression {
755
+ const { selectedColumns, joinClauses } = this.options;
756
+ const { alias } = joinClauses || {};
757
+ const cteName = `cte_distinct_${parentCTEName}`;
758
+ const fromName = `${parentCTEName}${alias ? ` AS ${this.entityNameFormatter(alias)}` : ''}`;
759
+ return {
760
+ type: ResolvedExpressionType.CTE,
761
+ cteName,
762
+ value: [
763
+ `${cteName} AS (SELECT DISTINCT ${selectedColumns.map((c) => `${alias ? `${this.entityNameFormatter(alias)}.` : ''}` + this.entityNameFormatter(c)).join(', ')} FROM ${fromName}${sqlParts.length ? ' ' + sqlParts.join(' ') : ''})`,
764
+ ],
765
+ };
766
+ }
767
+
768
+ private buildSqlQueryParts(columns: string[], isFinalProjection = false): string[] {
769
+ const sqlParts = [];
770
+
771
+ const where = [];
772
+ const filterCondition = this.getFilterExpression(new Set(columns));
773
+ if (filterCondition) {
774
+ where.push(filterCondition);
775
+ }
776
+ if (columns.length === 0 && this.buildState.isSuperFilterPending) {
777
+ const superFilterCondition = this.getSuperFilterExpression(isFinalProjection);
778
+ if (superFilterCondition) {
779
+ where.push(superFilterCondition);
780
+ }
781
+ }
782
+ if (columns.length === 0 && this.buildState.isSearchPending) {
783
+ this.buildState.isSearchPending = false;
784
+ where.push(this.options.searchExpression);
785
+ }
786
+ if (where.length) {
787
+ sqlParts.push(`WHERE ${where.join(' AND ')}`);
788
+ }
789
+
790
+ const sortExpression = this.getSortExpression(new Set(columns));
791
+ if (sortExpression) {
792
+ sqlParts.push(`ORDER BY ${sortExpression}`);
793
+ }
794
+
795
+ const paginationExpression = this.getPaginationExpression();
796
+ if (paginationExpression) {
797
+ sqlParts.push(paginationExpression);
798
+ }
799
+
800
+ return sqlParts;
801
+ }
802
+
803
+ /**
804
+ * Returns the calculation order for the given columns
805
+ * @private exposed for testing
806
+ */
807
+ public getCalculationOrder(columns: Set<string>): CalculationOrderResult {
808
+ const state = {
809
+ dependencyLevel: -1,
810
+ dependencyLevelMap: new Map<string, number>(),
811
+ nativeColumns: new Set<string>(),
812
+ visited: new Set<string>(),
813
+ };
814
+
815
+ // trigger dependency tracing for each column
816
+ columns.forEach((columnName) => {
817
+ state.dependencyLevel = -1; // reset dependency level for each column
818
+ this.dfs(columnName, state);
819
+ });
820
+
821
+ // build output
822
+ const formulaColumnsByDependencyLevel: IMDMColumnConfigWithParsedMeta[][] = [];
823
+ for (const [columnName, level] of state.dependencyLevelMap) {
824
+ const columnConfig = this.options.columnConfigMap[columnName];
825
+ if (level >= formulaColumnsByDependencyLevel.length) {
826
+ formulaColumnsByDependencyLevel.push([]);
827
+ }
828
+ formulaColumnsByDependencyLevel[level].push(columnConfig);
829
+ }
830
+
831
+ return {
832
+ nativeColumns: state.nativeColumns,
833
+ formulaColumnsByDependencyLevel,
834
+ };
835
+ }
836
+
837
+ private dfs(
838
+ columnName: string,
839
+ state: {
840
+ dependencyLevel: number;
841
+ dependencyLevelMap: Map<string, number>;
842
+ nativeColumns: Set<string>;
843
+ visited: Set<string>;
844
+ },
845
+ ) {
846
+ // If dependency level is already resolved, return
847
+ const level = state.dependencyLevelMap.get(columnName);
848
+ if (level !== undefined) {
849
+ if (level > state.dependencyLevel) {
850
+ state.dependencyLevel = level;
851
+ }
852
+ return;
853
+ }
854
+
855
+ if (state.visited.has(columnName)) {
856
+ throw new Error(`Cycle detected in formula dependencies: ${columnName}`);
857
+ }
858
+
859
+ const columnConfig = this.options.columnConfigMap[columnName];
860
+ const { columnType, columnMeta } = columnConfig;
861
+ const { sqlQueryProps, isCustomColumn, deps, defaultValueType } = columnMeta;
862
+
863
+ const isNativeColumn =
864
+ columnType !== MDM_COLUMN_TYPE.FORMULA && defaultValueType !== DEFAULT_VALUE_TYPE.DERIVED;
865
+ const isFormulaDbColumn =
866
+ columnType === MDM_COLUMN_TYPE.FORMULA && !sqlQueryProps && !isCustomColumn && !deps;
867
+ const isDerivedDbColumn =
868
+ defaultValueType === DEFAULT_VALUE_TYPE.DERIVED && !sqlQueryProps && !deps;
869
+
870
+ if (isNativeColumn || isFormulaDbColumn || isDerivedDbColumn) {
871
+ state.nativeColumns.add(columnName);
872
+ return;
873
+ }
874
+
875
+ // Skip processing to maintain backward compatibility with older formula visual columns
876
+ if (columnType === MDM_COLUMN_TYPE.FORMULA && isCustomColumn && !sqlQueryProps) {
877
+ return;
878
+ }
879
+
880
+ state.visited.add(columnName);
881
+ deps?.forEach((colName) => {
882
+ this.dfs(colName, state);
883
+ });
884
+
885
+ state.visited.delete(columnName);
886
+ state.dependencyLevelMap.set(columnName, ++state.dependencyLevel);
887
+ }
888
+
889
+ /**
890
+ * Applies the join positions from the query builder to the given Select AST.
891
+ * The join positions are added to the FROM list of the AST.
892
+ *
893
+ * @param {Select} ast - The Select AST to apply the join positions to.
894
+ * @returns {string[]} - An empty array, for consistency with other methods.
895
+ */
896
+ private applyJoinExpressionToMainCTE(ast: Select): From[] {
897
+ const { joinClauses, databaseDetails } = this.options;
898
+ const joinExpressions = getJoinExpressions(joinClauses as JoinExpressions, databaseDetails);
899
+ const joins: From[] = joinExpressions.map((j) => {
900
+ const raw: RawSqlExpression = { type: 'sql_expr', value: j };
901
+ return raw as unknown as From;
902
+ });
903
+ if (joins.length) {
904
+ ast.distinct = 'DISTINCT';
905
+ }
906
+ const joinFroms = ast.from as From[];
907
+ // set the FROM list as base table + joins
908
+ return joinFroms?.concat(joins);
909
+ }
910
+
911
+ /**
912
+ * Gets the super filter expression.
913
+ * If super filters are present and the pending filter config is not empty,
914
+ * it will process the super filter condition and return the result.
915
+ * If the super filter expression is empty, it will return an empty string.
916
+ * @returns {string} - The super filter expression.
917
+ */
918
+ private getSuperFilterExpression(isFinalProjection?: boolean): string {
919
+ if (!this.options.superFilters || !this.options.superFilters.children.length) return '';
920
+ const { superFilters, manualFilterOptions } = this.options;
921
+
922
+ const { children } = superFilters;
923
+ const { rankingFilters, nonRankingFilters } = children.reduce(
924
+ (acc, child) => {
925
+ if ('groupId' in child && 'children' in child) {
926
+ acc.nonRankingFilters.push(child);
927
+ } else if ('filters' in child) {
928
+ const filterType = (child as { filters: IGroupFilter }).filters.filterType;
929
+ if (filterType === 'RANKING') {
930
+ acc.rankingFilters.push(child);
931
+ } else {
932
+ acc.nonRankingFilters.push(child);
933
+ }
934
+ }
935
+ return acc;
936
+ },
937
+ {
938
+ rankingFilters: [] as typeof children,
939
+ nonRankingFilters: [] as typeof children,
940
+ },
941
+ );
942
+
943
+ let activeFilters = { ...superFilters, children: nonRankingFilters };
944
+ let joinClauses = this.options.joinClauses;
945
+
946
+ // Process ranking filters if pending and available
947
+ if (rankingFilters.length && isFinalProjection && this.buildState.isSuperFilterPending) {
948
+ activeFilters = { ...superFilters, children: rankingFilters };
949
+ joinClauses = { alias: '', joinRelation: [] };
950
+ }
951
+ if (rankingFilters.length === 0) {
952
+ this.buildState.isSuperFilterPending = false;
953
+ }
954
+ if (activeFilters.children.length) {
955
+ const sqlQueryBuilder = new SuperFilterBuilder(
956
+ activeFilters,
957
+ this.options.databaseDetails,
958
+ this.options.skipCountForRankingFilter,
959
+ joinClauses,
960
+ manualFilterOptions,
961
+ );
962
+ return sqlQueryBuilder.build();
963
+ }
964
+
965
+ return '';
966
+ }
967
+
968
+ private buildFinalQueryComponents(
969
+ currentCteName: string,
970
+ selectProjection: string,
971
+ isCountQuery: boolean,
972
+ distinct: boolean,
973
+ ): { cteDefinition: string; finalQuery: string; finalCteName: string } {
974
+ const { selectedColumns, enableLastSelectCTEWrap } = this.options;
975
+ let selectQuery = '*';
976
+ if (distinct && !isCountQuery) {
977
+ selectQuery = `DISTINCT ${selectedColumns
978
+ .map((c) => {
979
+ const schemaMatch = this.options.schema?.find((s) => s.name === c);
980
+ return this.entityNameFormatter(c, schemaMatch?.type ?? '');
981
+ })
982
+ .join(', ')}`;
983
+ }
984
+
985
+ let finalCteName = `${currentCteName}_main_cte`;
986
+ let cteDefinition = `${finalCteName} AS (${selectProjection})`;
987
+
988
+ const columns = isCountQuery ? `COUNT(${selectQuery}) AS row_count` : `${selectQuery}`;
989
+ const sqlParts = this.buildSqlQueryParts([], true);
990
+
991
+ const finalQuery = `SELECT ${columns} FROM ${finalCteName}${sqlParts.length ? ' ' + sqlParts.join(' ') : ''}`;
992
+
993
+ if (enableLastSelectCTEWrap && this.buildState.isSuperFilterPending) {
994
+ finalCteName = `${finalCteName}_main_cte`;
995
+ cteDefinition = ` ${cteDefinition},${finalCteName} AS (${finalQuery})`;
996
+ }
997
+
998
+ this.buildState.isSuperFilterPending = false;
999
+
1000
+ return { cteDefinition, finalQuery, finalCteName };
1001
+ }
1002
+
1003
+ private filterRequiredColumns(): Set<string> {
1004
+ const {
1005
+ selectedColumns,
1006
+ superFilters,
1007
+ primaryKeyColumns,
1008
+ sortConfig = [],
1009
+ joinClauses,
1010
+ } = this.options;
1011
+ const requiredColumn = new Set<string>(selectedColumns);
1012
+ if (superFilters?.children?.length) {
1013
+ // Collect all columnIds from superFilters
1014
+ (superFilters?.children as { filters: IGroupFilter }[])?.forEach((child) => {
1015
+ const filterColumn = child.filters?.columnIds?.[0];
1016
+ if (filterColumn) requiredColumn.add(filterColumn);
1017
+ child?.filters?.rankingFilter?.forEach?.((rank) => {
1018
+ if (rank?.rankByColumnName) requiredColumn.add(rank.rankByColumnName);
1019
+ });
1020
+ });
1021
+ primaryKeyColumns.forEach((col) => {
1022
+ requiredColumn.add(col);
1023
+ });
1024
+ sortConfig.forEach((col) => {
1025
+ requiredColumn.add(col?.column);
1026
+ });
1027
+ }
1028
+ // Add all columnIds from joinRelation
1029
+ for (const { id } of joinClauses?.joinRelation ?? []) {
1030
+ if (id != null) requiredColumn.add(id);
1031
+ }
1032
+
1033
+ return requiredColumn;
1034
+ }
1035
+ }