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;
17  
18  import java.sql.JDBCType;
19  import java.util.Objects;
20  import java.util.Optional;
21  
22  import org.jspecify.annotations.Nullable;
23  import org.mybatis.dynamic.sql.render.RenderingContext;
24  import org.mybatis.dynamic.sql.render.RenderingStrategy;
25  import org.mybatis.dynamic.sql.util.FragmentAndParameters;
26  import org.mybatis.dynamic.sql.util.StringUtilities;
27  
28  public class SqlColumn<T> implements BindableColumn<T>, SortSpecification {
29  
30      protected final String name;
31      protected final SqlTable table;
32      protected final @Nullable JDBCType jdbcType;
33      protected final String descendingPhrase;
34      protected final @Nullable String alias;
35      protected final @Nullable String typeHandler;
36      protected final @Nullable RenderingStrategy renderingStrategy;
37      protected final ParameterTypeConverter<T, ?> parameterTypeConverter;
38      protected final @Nullable String tableQualifier;
39      protected final @Nullable Class<T> javaType;
40  
41      private SqlColumn(Builder<T> builder) {
42          name = Objects.requireNonNull(builder.name);
43          table = Objects.requireNonNull(builder.table);
44          jdbcType = builder.jdbcType;
45          descendingPhrase = builder.descendingPhrase;
46          alias = builder.alias;
47          typeHandler = builder.typeHandler;
48          renderingStrategy = builder.renderingStrategy;
49          parameterTypeConverter = Objects.requireNonNull(builder.parameterTypeConverter);
50          tableQualifier = builder.tableQualifier;
51          javaType = builder.javaType;
52      }
53  
54      public String name() {
55          return name;
56      }
57  
58      public SqlTable table() {
59          return table;
60      }
61  
62      @Override
63      public Optional<JDBCType> jdbcType() {
64          return Optional.ofNullable(jdbcType);
65      }
66  
67      @Override
68      public Optional<String> alias() {
69          return Optional.ofNullable(alias);
70      }
71  
72      @Override
73      public Optional<String> typeHandler() {
74          return Optional.ofNullable(typeHandler);
75      }
76  
77      @Override
78      public Optional<Class<T>> javaType() {
79          return Optional.ofNullable(javaType);
80      }
81  
82      @Override
83      public @Nullable Object convertParameterType(@Nullable T value) {
84          return value == null ? null : parameterTypeConverter.convert(value);
85      }
86  
87      @Override
88      public SortSpecification descending() {
89          Builder<T> b = copy();
90          return b.withDescendingPhrase(" DESC").build(); //$NON-NLS-1$
91      }
92  
93      @Override
94      public SqlColumn<T> as(String alias) {
95          Builder<T> b = copy();
96          return b.withAlias(alias).build();
97      }
98  
99      /**
100      * Override the calculated table qualifier if there is one. This is useful for sub-queries
101      * where the calculated table qualifier may not be correct in all cases.
102      *
103      * @param tableQualifier the table qualifier to apply to the rendered column name
104      * @return a new column that will be rendered with the specified table qualifier
105      */
106     public SqlColumn<T> qualifiedWith(String tableQualifier) {
107         Builder<T> b = copy();
108         b.withTableQualifier(tableQualifier);
109         return b.build();
110     }
111 
112     /**
113      * Set an alias with a camel cased string based on the column name. This can be useful for queries using
114      * the {@link org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper} where the columns are placed into
115      * a map based on the column name returned from the database.
116      *
117      * <p>A camel case string is mixed case, and most databases do not support unquoted mixed case strings
118      * as identifiers. Therefore, the generated alias will be surrounded by double quotes thereby making it a
119      * quoted identifier. Most databases will respect quoted mixed case identifiers.
120      *
121      * @return a new column aliased with a camel case version of the column name
122      */
123     public SqlColumn<T> asCamelCase() {
124         Builder<T> b = copy();
125         return b.withAlias("\"" + StringUtilities.toCamelCase(name) + "\"").build(); //$NON-NLS-1$ //$NON-NLS-2$
126     }
127 
128     @Override
129     public FragmentAndParameters renderForOrderBy(RenderingContext renderingContext) {
130         return FragmentAndParameters.fromFragment(alias().orElse(name) + descendingPhrase);
131     }
132 
133     @Override
134     public FragmentAndParameters render(RenderingContext renderingContext) {
135         if (tableQualifier == null) {
136             return FragmentAndParameters.fromFragment(renderingContext.aliasedColumnName(this));
137         } else {
138             return FragmentAndParameters.fromFragment(renderingContext.aliasedColumnName(this, tableQualifier));
139         }
140     }
141 
142     @Override
143     public Optional<RenderingStrategy> renderingStrategy() {
144         return Optional.ofNullable(renderingStrategy);
145     }
146 
147     public <S> SqlColumn<S> withTypeHandler(String typeHandler) {
148         Builder<S> b = copy();
149         return b.withTypeHandler(typeHandler).build();
150     }
151 
152     public <S> SqlColumn<S> withRenderingStrategy(RenderingStrategy renderingStrategy) {
153         Builder<S> b = copy();
154         return b.withRenderingStrategy(renderingStrategy).build();
155     }
156 
157     public <S> SqlColumn<S> withParameterTypeConverter(ParameterTypeConverter<S, ?> parameterTypeConverter) {
158         Builder<S> b = copy();
159         return b.withParameterTypeConverter(parameterTypeConverter).build();
160     }
161 
162     public <S> SqlColumn<S> withJavaType(Class<S> javaType) {
163         Builder<S> b = copy();
164         return b.withJavaType(javaType).build();
165     }
166 
167     /**
168      * This method helps us tell a bit of fiction to the Java compiler. Java, for better or worse,
169      * does not carry generic type information through chained methods. We want to enable method
170      * chaining in the "with" methods. With this bit of fiction, we force the compiler to delay type
171      * inference to the last method in the chain.
172      *
173      * @param <S> the type. Will be the same as T for this usage.
174      * @return a new SqlColumn of type S (S is the same as T)
175      */
176     @SuppressWarnings("unchecked")
177     private <S> Builder<S> copy() {
178         return new Builder<S>()
179                 .withName(this.name)
180                 .withTable(this.table)
181                 .withJdbcType(this.jdbcType)
182                 .withDescendingPhrase(this.descendingPhrase)
183                 .withAlias(this.alias)
184                 .withTypeHandler(this.typeHandler)
185                 .withRenderingStrategy(this.renderingStrategy)
186                 .withParameterTypeConverter((ParameterTypeConverter<S, ?>) this.parameterTypeConverter)
187                 .withTableQualifier(this.tableQualifier)
188                 .withJavaType((Class<S>) this.javaType);
189     }
190 
191     public static <T> SqlColumn<T> of(String name, SqlTable table) {
192         return new Builder<T>().withName(name)
193                 .withTable(table)
194                 .build();
195     }
196 
197     public static <T> SqlColumn<T> of(String name, SqlTable table, JDBCType jdbcType) {
198         return new Builder<T>().withName(name)
199                 .withTable(table)
200                 .withJdbcType(jdbcType)
201                 .build();
202     }
203 
204     public static class Builder<T> {
205         protected @Nullable String name;
206         protected @Nullable SqlTable table;
207         protected @Nullable JDBCType jdbcType;
208         protected String descendingPhrase = ""; //$NON-NLS-1$
209         protected @Nullable String alias;
210         protected @Nullable String typeHandler;
211         protected @Nullable RenderingStrategy renderingStrategy;
212         protected ParameterTypeConverter<T, ?> parameterTypeConverter = v -> v;
213         protected @Nullable String tableQualifier;
214         protected @Nullable Class<T> javaType;
215 
216         public Builder<T> withName(String name) {
217             this.name = name;
218             return this;
219         }
220 
221         public Builder<T> withTable(SqlTable table) {
222             this.table = table;
223             return this;
224         }
225 
226         public Builder<T> withJdbcType(@Nullable JDBCType jdbcType) {
227             this.jdbcType = jdbcType;
228             return this;
229         }
230 
231         public Builder<T> withDescendingPhrase(String descendingPhrase) {
232             this.descendingPhrase = descendingPhrase;
233             return this;
234         }
235 
236         public Builder<T> withAlias(@Nullable String alias) {
237             this.alias = alias;
238             return this;
239         }
240 
241         public Builder<T> withTypeHandler(@Nullable String typeHandler) {
242             this.typeHandler = typeHandler;
243             return this;
244         }
245 
246         public Builder<T> withRenderingStrategy(@Nullable RenderingStrategy renderingStrategy) {
247             this.renderingStrategy = renderingStrategy;
248             return this;
249         }
250 
251         public Builder<T> withParameterTypeConverter(ParameterTypeConverter<T, ?> parameterTypeConverter) {
252             this.parameterTypeConverter = parameterTypeConverter;
253             return this;
254         }
255 
256         private Builder<T> withTableQualifier(@Nullable String tableQualifier) {
257             this.tableQualifier = tableQualifier;
258             return this;
259         }
260 
261         public Builder<T> withJavaType(@Nullable Class<T> javaType) {
262             this.javaType = javaType;
263             return this;
264         }
265 
266         public SqlColumn<T> build() {
267             return new SqlColumn<>(this);
268         }
269     }
270 }