|
| 1 | +/** |
| 2 | + * ObjectUI - Query AST Builder |
| 3 | + * Phase 3.3: QuerySchema AST implementation |
| 4 | + */ |
| 5 | + |
| 6 | +import type { |
| 7 | + QueryAST, |
| 8 | + QuerySchema, |
| 9 | + SelectNode, |
| 10 | + FromNode, |
| 11 | + WhereNode, |
| 12 | + JoinNode, |
| 13 | + GroupByNode, |
| 14 | + OrderByNode, |
| 15 | + LimitNode, |
| 16 | + OffsetNode, |
| 17 | + AggregateNode, |
| 18 | + FieldNode, |
| 19 | + LiteralNode, |
| 20 | + OperatorNode, |
| 21 | + LogicalOperator, |
| 22 | + AdvancedFilterSchema, |
| 23 | + AdvancedFilterCondition, |
| 24 | + QuerySortConfig, |
| 25 | + JoinConfig, |
| 26 | + AggregationConfig, |
| 27 | +} from '@object-ui/types'; |
| 28 | + |
| 29 | +/** |
| 30 | + * Query AST Builder - Converts QuerySchema to AST |
| 31 | + */ |
| 32 | +export class QueryASTBuilder { |
| 33 | + build(query: QuerySchema): QueryAST { |
| 34 | + const ast: QueryAST = { |
| 35 | + select: this.buildSelect(query), |
| 36 | + from: this.buildFrom(query), |
| 37 | + }; |
| 38 | + |
| 39 | + if (query.filter) { |
| 40 | + ast.where = this.buildWhere(query.filter); |
| 41 | + } |
| 42 | + |
| 43 | + if (query.joins && query.joins.length > 0) { |
| 44 | + ast.joins = query.joins.map(join => this.buildJoin(join)); |
| 45 | + } |
| 46 | + |
| 47 | + if (query.group_by && query.group_by.length > 0) { |
| 48 | + ast.group_by = this.buildGroupBy(query.group_by); |
| 49 | + } |
| 50 | + |
| 51 | + if (query.sort && query.sort.length > 0) { |
| 52 | + ast.order_by = this.buildOrderBy(query.sort); |
| 53 | + } |
| 54 | + |
| 55 | + if (query.limit !== undefined) { |
| 56 | + ast.limit = this.buildLimit(query.limit); |
| 57 | + } |
| 58 | + |
| 59 | + if (query.offset !== undefined) { |
| 60 | + ast.offset = this.buildOffset(query.offset); |
| 61 | + } |
| 62 | + |
| 63 | + return ast; |
| 64 | + } |
| 65 | + |
| 66 | + private buildSelect(query: QuerySchema): SelectNode { |
| 67 | + const fields: (FieldNode | AggregateNode)[] = []; |
| 68 | + |
| 69 | + if (query.fields && query.fields.length > 0) { |
| 70 | + fields.push(...query.fields.map(field => this.buildField(field))); |
| 71 | + } else { |
| 72 | + fields.push(this.buildField('*')); |
| 73 | + } |
| 74 | + |
| 75 | + if (query.aggregations && query.aggregations.length > 0) { |
| 76 | + fields.push(...query.aggregations.map(agg => this.buildAggregation(agg))); |
| 77 | + } |
| 78 | + |
| 79 | + return { |
| 80 | + type: 'select', |
| 81 | + fields, |
| 82 | + distinct: false, |
| 83 | + }; |
| 84 | + } |
| 85 | + |
| 86 | + private buildFrom(query: QuerySchema): FromNode { |
| 87 | + return { |
| 88 | + type: 'from', |
| 89 | + table: query.object, |
| 90 | + }; |
| 91 | + } |
| 92 | + |
| 93 | + private buildWhere(filter: AdvancedFilterSchema): WhereNode { |
| 94 | + return { |
| 95 | + type: 'where', |
| 96 | + condition: this.buildFilterCondition(filter), |
| 97 | + }; |
| 98 | + } |
| 99 | + |
| 100 | + private buildFilterCondition(filter: AdvancedFilterSchema): OperatorNode { |
| 101 | + const operator = filter.operator || 'and'; |
| 102 | + const operands: (OperatorNode | FieldNode | LiteralNode)[] = []; |
| 103 | + |
| 104 | + if (filter.conditions && filter.conditions.length > 0) { |
| 105 | + operands.push(...filter.conditions.map(cond => this.buildCondition(cond))); |
| 106 | + } |
| 107 | + |
| 108 | + if (filter.groups && filter.groups.length > 0) { |
| 109 | + operands.push(...filter.groups.map(group => this.buildFilterCondition(group))); |
| 110 | + } |
| 111 | + |
| 112 | + return { |
| 113 | + type: 'operator', |
| 114 | + operator: operator as LogicalOperator, |
| 115 | + operands, |
| 116 | + }; |
| 117 | + } |
| 118 | + |
| 119 | + private buildCondition(condition: AdvancedFilterCondition): OperatorNode { |
| 120 | + const field = this.buildField(condition.field); |
| 121 | + const value = this.buildLiteral(condition.value); |
| 122 | + |
| 123 | + return { |
| 124 | + type: 'operator', |
| 125 | + operator: '=', |
| 126 | + operands: [field, value], |
| 127 | + }; |
| 128 | + } |
| 129 | + |
| 130 | + private buildJoin(join: JoinConfig): JoinNode { |
| 131 | + const onCondition: OperatorNode = { |
| 132 | + type: 'operator', |
| 133 | + operator: '=', |
| 134 | + operands: [ |
| 135 | + this.buildField(join.on.local_field), |
| 136 | + this.buildField(join.on.foreign_field, join.alias || join.object), |
| 137 | + ], |
| 138 | + }; |
| 139 | + |
| 140 | + return { |
| 141 | + type: 'join', |
| 142 | + join_type: join.type, |
| 143 | + table: join.object, |
| 144 | + alias: join.alias, |
| 145 | + on: onCondition, |
| 146 | + }; |
| 147 | + } |
| 148 | + |
| 149 | + private buildGroupBy(fields: string[]): GroupByNode { |
| 150 | + return { |
| 151 | + type: 'group_by', |
| 152 | + fields: fields.map(field => this.buildField(field)), |
| 153 | + }; |
| 154 | + } |
| 155 | + |
| 156 | + private buildOrderBy(sorts: QuerySortConfig[]): OrderByNode { |
| 157 | + return { |
| 158 | + type: 'order_by', |
| 159 | + fields: sorts.map(sort => ({ |
| 160 | + field: this.buildField(sort.field), |
| 161 | + direction: sort.order, |
| 162 | + })), |
| 163 | + }; |
| 164 | + } |
| 165 | + |
| 166 | + private buildLimit(limit: number): LimitNode { |
| 167 | + return { |
| 168 | + type: 'limit', |
| 169 | + value: limit, |
| 170 | + }; |
| 171 | + } |
| 172 | + |
| 173 | + private buildOffset(offset: number): OffsetNode { |
| 174 | + return { |
| 175 | + type: 'offset', |
| 176 | + value: offset, |
| 177 | + }; |
| 178 | + } |
| 179 | + |
| 180 | + private buildField(field: string, table?: string): FieldNode { |
| 181 | + const parts = field.split('.'); |
| 182 | + |
| 183 | + if (parts.length === 2) { |
| 184 | + return { |
| 185 | + type: 'field', |
| 186 | + table: parts[0], |
| 187 | + name: parts[1], |
| 188 | + }; |
| 189 | + } |
| 190 | + |
| 191 | + return { |
| 192 | + type: 'field', |
| 193 | + table, |
| 194 | + name: field, |
| 195 | + }; |
| 196 | + } |
| 197 | + |
| 198 | + private buildLiteral(value: any): LiteralNode { |
| 199 | + let dataType: 'string' | 'number' | 'boolean' | 'date' | 'null' = 'string'; |
| 200 | + |
| 201 | + if (value === null || value === undefined) { |
| 202 | + dataType = 'null'; |
| 203 | + } else if (typeof value === 'number') { |
| 204 | + dataType = 'number'; |
| 205 | + } else if (typeof value === 'boolean') { |
| 206 | + dataType = 'boolean'; |
| 207 | + } else if (value instanceof Date) { |
| 208 | + dataType = 'date'; |
| 209 | + } |
| 210 | + |
| 211 | + return { |
| 212 | + type: 'literal', |
| 213 | + value, |
| 214 | + data_type: dataType, |
| 215 | + }; |
| 216 | + } |
| 217 | + |
| 218 | + private buildAggregation(agg: AggregationConfig): AggregateNode { |
| 219 | + return { |
| 220 | + type: 'aggregate', |
| 221 | + function: agg.function, |
| 222 | + field: agg.field ? this.buildField(agg.field) : undefined, |
| 223 | + alias: agg.alias, |
| 224 | + distinct: agg.distinct, |
| 225 | + }; |
| 226 | + } |
| 227 | + |
| 228 | + optimize(ast: QueryAST): QueryAST { |
| 229 | + return ast; |
| 230 | + } |
| 231 | +} |
| 232 | + |
| 233 | +export const defaultQueryASTBuilder = new QueryASTBuilder(); |
| 234 | + |
| 235 | +export function buildQueryAST(query: QuerySchema): QueryAST { |
| 236 | + return defaultQueryASTBuilder.build(query); |
| 237 | +} |
0 commit comments