View Javadoc
1   /*
2    *    Copyright 2016-2025 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.mybatis.dynamic.sql.select.render;
17  
18  import java.util.Objects;
19  import java.util.Optional;
20  import java.util.stream.Collectors;
21  
22  import org.jspecify.annotations.Nullable;
23  import org.mybatis.dynamic.sql.BasicColumn;
24  import org.mybatis.dynamic.sql.TableExpression;
25  import org.mybatis.dynamic.sql.render.ExplicitTableAliasCalculator;
26  import org.mybatis.dynamic.sql.render.GuaranteedTableAliasCalculator;
27  import org.mybatis.dynamic.sql.render.RenderingContext;
28  import org.mybatis.dynamic.sql.render.TableAliasCalculator;
29  import org.mybatis.dynamic.sql.select.GroupByModel;
30  import org.mybatis.dynamic.sql.select.HavingModel;
31  import org.mybatis.dynamic.sql.select.QueryExpressionModel;
32  import org.mybatis.dynamic.sql.select.join.JoinModel;
33  import org.mybatis.dynamic.sql.util.FragmentAndParameters;
34  import org.mybatis.dynamic.sql.util.FragmentCollector;
35  import org.mybatis.dynamic.sql.util.StringUtilities;
36  import org.mybatis.dynamic.sql.where.EmbeddedWhereModel;
37  
38  public class QueryExpressionRenderer {
39      private final QueryExpressionModel queryExpression;
40      private final TableExpressionRenderer tableExpressionRenderer;
41      private final RenderingContext renderingContext;
42  
43      private QueryExpressionRenderer(Builder builder) {
44          queryExpression = Objects.requireNonNull(builder.queryExpression);
45          TableAliasCalculator childTableAliasCalculator = calculateChildTableAliasCalculator(queryExpression);
46  
47          renderingContext = Objects.requireNonNull(builder.renderingContext)
48                  .withChildTableAliasCalculator(childTableAliasCalculator);
49  
50          tableExpressionRenderer = new TableExpressionRenderer.Builder()
51                  .withRenderingContext(renderingContext)
52                  .build();
53      }
54  
55      /**
56       * This function calculates a table alias calculator to use in the current context. There are several
57       * possibilities: this could be a renderer for a top level select statement, or it could be a renderer for a table
58       * expression in a join, or a column to sub query where condition, or it could be a renderer for a select
59       * statement in an "exists" condition in a where clause.
60       *
61       * <p>In the case of conditions in a where clause, we will have a parent table alias calculator. This will give
62       * visibility to the aliases in the outer select statement to this renderer so columns in aliased tables can be
63       * used in where clause sub query conditions without having to re-specify the alias.
64       *
65       * <p>Another complication is that we calculate aliases differently if there are joins and sub queries. The
66       * cases are as follows:
67       *
68       * <ol>
69       *     <li>If there are no joins, then we will only use aliases that are explicitly set by the user</li>
70       *     <lI>If there are joins and sub queries, we will also only use explicit aliases</lI>
71       *     <li>If there are joins, but no sub queries, then we will automatically use the table name
72       *     as an alias if no explicit alias has been specified</li>
73       * </ol>
74       *
75       * @param queryExpression the model to render
76       * @return a table alias calculator appropriate for this context
77       */
78      private TableAliasCalculator calculateChildTableAliasCalculator(QueryExpressionModel queryExpression) {
79          return queryExpression.joinModel()
80                  .map(JoinModel::containsSubQueries)
81                  .map(this::calculateTableAliasCalculatorWithJoins)
82                  .orElseGet(this::explicitTableAliasCalculator);
83      }
84  
85      private TableAliasCalculator calculateTableAliasCalculatorWithJoins(boolean hasSubQueries) {
86          if (hasSubQueries) {
87              // if there are subqueries, we cannot use the table name automatically
88              // so all aliases must be specified
89              return explicitTableAliasCalculator();
90          } else {
91              // without subqueries, we can automatically use table names as aliases
92              return guaranteedTableAliasCalculator();
93          }
94      }
95  
96      private TableAliasCalculator explicitTableAliasCalculator() {
97          return ExplicitTableAliasCalculator.of(queryExpression.tableAliases());
98      }
99  
100     private TableAliasCalculator guaranteedTableAliasCalculator() {
101         return GuaranteedTableAliasCalculator.of(queryExpression.tableAliases());
102     }
103 
104     public FragmentAndParameters render() {
105         FragmentCollector fragmentCollector = new FragmentCollector();
106 
107         fragmentCollector.add(calculateQueryExpressionStart());
108         calculateJoinClause().ifPresent(fragmentCollector::add);
109         calculateWhereClause().ifPresent(fragmentCollector::add);
110         calculateGroupByClause().ifPresent(fragmentCollector::add);
111         calculateHavingClause().ifPresent(fragmentCollector::add);
112 
113         return fragmentCollector.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$
114     }
115 
116     private FragmentAndParameters calculateQueryExpressionStart() {
117         FragmentAndParameters columnList = calculateColumnList();
118 
119         String start = queryExpression.connector().map(StringUtilities::spaceAfter).orElse("") //$NON-NLS-1$
120                 + "select " //$NON-NLS-1$
121                 + (queryExpression.isDistinct() ? "distinct " : "") //$NON-NLS-1$ //$NON-NLS-2$
122                 + columnList.fragment()
123                 + " from "; //$NON-NLS-1$
124 
125         FragmentAndParameters renderedTable = renderTableExpression(queryExpression.table());
126         start += renderedTable.fragment();
127 
128         return FragmentAndParameters.withFragment(start)
129                 .withParameters(renderedTable.parameters())
130                 .withParameters(columnList.parameters())
131                 .build();
132     }
133 
134     private FragmentAndParameters calculateColumnList() {
135         return queryExpression.columns()
136                 .map(this::renderColumnAndAlias)
137                 .collect(FragmentCollector.collect())
138                 .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$
139     }
140 
141     private FragmentAndParameters renderColumnAndAlias(BasicColumn selectListItem) {
142         FragmentAndParameters renderedColumn = selectListItem.render(renderingContext);
143 
144         return selectListItem.alias().map(a -> renderedColumn.mapFragment(f -> f + " as " + a)) //$NON-NLS-1$
145                 .orElse(renderedColumn);
146     }
147 
148     private FragmentAndParameters renderTableExpression(TableExpression table) {
149         return table.accept(tableExpressionRenderer);
150     }
151 
152     private Optional<FragmentAndParameters> calculateJoinClause() {
153         return queryExpression.joinModel().map(this::renderJoin);
154     }
155 
156     private FragmentAndParameters renderJoin(JoinModel joinModel) {
157         return JoinRenderer.withJoinModel(joinModel)
158                 .withTableExpressionRenderer(tableExpressionRenderer)
159                 .withRenderingContext(renderingContext)
160                 .build()
161                 .render();
162     }
163 
164     private Optional<FragmentAndParameters> calculateWhereClause() {
165         return queryExpression.whereModel().flatMap(this::renderWhereClause);
166     }
167 
168     private Optional<FragmentAndParameters> renderWhereClause(EmbeddedWhereModel whereModel) {
169         return whereModel.render(renderingContext);
170     }
171 
172     private Optional<FragmentAndParameters> calculateGroupByClause() {
173         return queryExpression.groupByModel().map(this::renderGroupBy);
174     }
175 
176     private FragmentAndParameters renderGroupBy(GroupByModel groupByModel) {
177         return groupByModel.columns()
178                 .map(this::renderColumn)
179                 .collect(FragmentCollector.collect())
180                 .toFragmentAndParameters(
181                         Collectors.joining(", ", "group by ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$)
182     }
183 
184     private FragmentAndParameters renderColumn(BasicColumn column) {
185         return column.render(renderingContext);
186     }
187 
188     private Optional<FragmentAndParameters> calculateHavingClause() {
189         return queryExpression.havingModel().flatMap(this::renderHavingClause);
190     }
191 
192     private Optional<FragmentAndParameters> renderHavingClause(HavingModel havingModel) {
193         return HavingRenderer.withHavingModel(havingModel)
194                 .withRenderingContext(renderingContext)
195                 .build()
196                 .render();
197     }
198 
199     public static Builder withQueryExpression(QueryExpressionModel model) {
200         return new Builder().withQueryExpression(model);
201     }
202 
203     public static class Builder {
204         private @Nullable QueryExpressionModel queryExpression;
205         private @Nullable RenderingContext renderingContext;
206 
207         public Builder withRenderingContext(RenderingContext renderingContext) {
208             this.renderingContext = renderingContext;
209             return this;
210         }
211 
212         public Builder withQueryExpression(QueryExpressionModel queryExpression) {
213             this.queryExpression = queryExpression;
214             return this;
215         }
216 
217         public QueryExpressionRenderer build() {
218             return new QueryExpressionRenderer(this);
219         }
220     }
221 }