diff --git a/CHANGELOG.md b/CHANGELOG.md index 58af4165e..8e622653f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ Kotlin DSL. - Added a new sort specification that is useful in selects with joins ([#269](https://github.com/mybatis/mybatis-dynamic-sql/pull/269)) - Added the capability to generate a camel cased alias for a column ([#272](https://github.com/mybatis/mybatis-dynamic-sql/issues/272)) +- Added sub-query support for "from" clauses in a select statement ([#282](https://github.com/mybatis/mybatis-dynamic-sql/pull/282)) +- Added Kotlin DSL updates to support sub-queries in select statements, where clauses, and insert statements ([#282](https://github.com/mybatis/mybatis-dynamic-sql/pull/282)) ## Release 1.2.1 - September 29, 2020 diff --git a/src/main/java/org/mybatis/dynamic/sql/DerivedColumn.java b/src/main/java/org/mybatis/dynamic/sql/DerivedColumn.java new file mode 100644 index 000000000..a2e5d37b7 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/DerivedColumn.java @@ -0,0 +1,129 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql; + +import java.sql.JDBCType; +import java.util.Objects; +import java.util.Optional; + +import org.mybatis.dynamic.sql.render.TableAliasCalculator; + +/** + * A derived column is a column that is not directly related to a table. This is primarily + * used for supporting sub-queries. The main difference in this class and {@link SqlColumn} is + * that this class does not have a related {@link SqlTable} and therefore ignores any table + * qualifier set in a query. If a table qualifier is required it can be set directly in the + * builder for this class. + * + * @param The Java type that corresponds to this column - not used except for compiler type checking + * for conditions + */ +public class DerivedColumn implements BindableColumn { + private final String name; + private final String tableQualifier; + private final String columnAlias; + private final JDBCType jdbcType; + private final String typeHandler; + + protected DerivedColumn(Builder builder) { + this.name = Objects.requireNonNull(builder.name); + this.tableQualifier = builder.tableQualifier; + this.columnAlias = builder.columnAlias; + this.jdbcType = builder.jdbcType; + this.typeHandler = builder.typeHandler; + } + + @Override + public Optional alias() { + return Optional.ofNullable(columnAlias); + } + + @Override + public Optional jdbcType() { + return Optional.ofNullable(jdbcType); + } + + @Override + public Optional typeHandler() { + return Optional.ofNullable(typeHandler); + } + + @Override + public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { + return tableQualifier == null ? name : tableQualifier + "." + name; //$NON-NLS-1$ + } + + @Override + public DerivedColumn as(String columnAlias) { + return new Builder() + .withName(name) + .withColumnAlias(columnAlias) + .withJdbcType(jdbcType) + .withTypeHandler(typeHandler) + .withTableQualifier(tableQualifier) + .build(); + } + + public static DerivedColumn of(String name) { + return new Builder() + .withName(name) + .build(); + } + + public static DerivedColumn of(String name, String tableQualifier) { + return new Builder() + .withName(name) + .withTableQualifier(tableQualifier) + .build(); + } + + public static class Builder { + private String name; + private String tableQualifier; + private String columnAlias; + private JDBCType jdbcType; + private String typeHandler; + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withTableQualifier(String tableQualifier) { + this.tableQualifier = tableQualifier; + return this; + } + + public Builder withColumnAlias(String columnAlias) { + this.columnAlias = columnAlias; + return this; + } + + public Builder withJdbcType(JDBCType jdbcType) { + this.jdbcType = jdbcType; + return this; + } + + public Builder withTypeHandler(String typeHandler) { + this.typeHandler = typeHandler; + return this; + } + + public DerivedColumn build() { + return new DerivedColumn<>(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 5ecf7b4eb..ee67fe139 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Objects; import java.util.function.Supplier; @@ -760,6 +761,11 @@ public InsertSelectDSL.SelectGatherer withColumnList(SqlColumn...columns) { .withColumnList(columns); } + public InsertSelectDSL.SelectGatherer withColumnList(List> columns) { + return InsertSelectDSL.insertInto(table) + .withColumnList(columns); + } + public GeneralInsertDSL.SetClauseFinisher set(SqlColumn column) { return GeneralInsertDSL.insertInto(table) .set(column); diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java b/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java index 0c8cf5de5..fa76a170d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java @@ -18,6 +18,7 @@ import java.sql.JDBCType; import java.util.Objects; import java.util.Optional; +import java.util.function.BiFunction; import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.render.RenderingStrategy; @@ -34,6 +35,7 @@ public class SqlColumn implements BindableColumn, SortSpecification { protected final String typeHandler; protected final RenderingStrategy renderingStrategy; protected final ParameterTypeConverter parameterTypeConverter; + protected final BiFunction> tableQualifierFunction; private SqlColumn(Builder builder) { name = Objects.requireNonNull(builder.name); @@ -44,6 +46,7 @@ private SqlColumn(Builder builder) { typeHandler = builder.typeHandler; renderingStrategy = builder.renderingStrategy; parameterTypeConverter = builder.parameterTypeConverter; + tableQualifierFunction = Objects.requireNonNull(builder.tableQualifierFunction); } public String name() { @@ -86,6 +89,19 @@ public SqlColumn as(String alias) { return b.withAlias(alias).build(); } + /** + * Override the calculated table qualifier if there is one. This is useful for sub-queries + * where the calculated table qualifier may not be correct in all cases. + * + * @param tableQualifier the table qualifier to apply to the rendered column name + * @return a new column that will be rendered with the specified table qualifier + */ + public SqlColumn qualifiedWith(String tableQualifier) { + Builder b = copy(); + b.withTableQualifierFunction((tac, t) -> Optional.of(tableQualifier)); + return b.build(); + } + /** * Set an alias with a camel cased string based on the column name. The can be useful for queries using * the {@link org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper} where the columns are placed into @@ -114,7 +130,7 @@ public String orderByName() { @Override public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { - return tableAliasCalculator.aliasForColumn(table) + return tableQualifierFunction.apply(tableAliasCalculator, table) .map(this::applyTableAlias) .orElseGet(this::name); } @@ -160,8 +176,9 @@ private Builder copy() { .withDescending(this.isDescending) .withAlias(this.alias) .withTypeHandler(this.typeHandler) - .withRenderingStrategy((this.renderingStrategy)) - .withParameterTypeConverter((ParameterTypeConverter) this.parameterTypeConverter); + .withRenderingStrategy(this.renderingStrategy) + .withParameterTypeConverter((ParameterTypeConverter) this.parameterTypeConverter) + .withTableQualifierFunction(this.tableQualifierFunction); } private String applyTableAlias(String tableAlias) { @@ -190,6 +207,8 @@ public static class Builder { protected String typeHandler; protected RenderingStrategy renderingStrategy; protected ParameterTypeConverter parameterTypeConverter; + protected BiFunction> tableQualifierFunction = + TableAliasCalculator::aliasForColumn; public Builder withName(String name) { this.name = name; @@ -231,6 +250,12 @@ public Builder withParameterTypeConverter(ParameterTypeConverter parame return this; } + private Builder withTableQualifierFunction( + BiFunction> tableQualifierFunction) { + this.tableQualifierFunction = tableQualifierFunction; + return this; + } + public SqlColumn build() { return new SqlColumn<>(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlTable.java b/src/main/java/org/mybatis/dynamic/sql/SqlTable.java index 0bbaad7cb..664ccd321 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlTable.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlTable.java @@ -22,7 +22,7 @@ import org.jetbrains.annotations.NotNull; -public class SqlTable { +public class SqlTable implements TableExpression { private final Supplier nameSupplier; @@ -103,6 +103,11 @@ public SqlColumn column(String name, JDBCType jdbcType, String typeHandle return column.withTypeHandler(typeHandler); } + @Override + public R accept(TableExpressionVisitor visitor) { + return visitor.visit(this); + } + public static SqlTable of(String name) { return new SqlTable(name); } diff --git a/src/main/java/org/mybatis/dynamic/sql/TableExpression.java b/src/main/java/org/mybatis/dynamic/sql/TableExpression.java new file mode 100644 index 000000000..e0722894a --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/TableExpression.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql; + +public interface TableExpression { + + R accept(TableExpressionVisitor visitor); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/TableExpressionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/TableExpressionVisitor.java new file mode 100644 index 000000000..1395a367f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/TableExpressionVisitor.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql; + +import org.mybatis.dynamic.sql.select.SubQuery; + +public interface TableExpressionVisitor { + R visit(SqlTable table); + + R visit(SubQuery subQuery); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectDSL.java b/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectDSL.java index 323e23cc0..79afacab0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectDSL.java @@ -61,7 +61,11 @@ private InsertColumnGatherer(SqlTable table) { } public SelectGatherer withColumnList(SqlColumn...columns) { - return new SelectGatherer(table, Arrays.asList(columns)); + return withColumnList(Arrays.asList(columns)); + } + + public SelectGatherer withColumnList(List> columns) { + return new SelectGatherer(table, columns); } public InsertSelectDSL withSelectStatement(Buildable selectModelBuilder) { diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectModel.java index ef280a17d..ebca00c1e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/InsertSelectModel.java @@ -18,6 +18,7 @@ import java.util.Objects; import java.util.Optional; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.insert.render.InsertSelectRenderer; import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider; @@ -47,6 +48,7 @@ public Optional columnList() { return Optional.ofNullable(columnList); } + @NotNull public InsertSelectStatementProvider render(RenderingStrategy renderingStrategy) { return InsertSelectRenderer.withInsertSelectModel(this) .withRenderingStrategy(renderingStrategy) diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java index d6db21ad9..33287616e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java @@ -25,6 +25,7 @@ import java.util.stream.Collectors; import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.TableExpression; import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.select.join.JoinModel; import org.mybatis.dynamic.sql.select.join.JoinSpecification; @@ -36,13 +37,13 @@ public abstract class AbstractQueryExpressionDSL joinSpecificationBuilders = new ArrayList<>(); protected final Map tableAliases = new HashMap<>(); - private final SqlTable table; + private final TableExpression table; - protected AbstractQueryExpressionDSL(SqlTable table) { + protected AbstractQueryExpressionDSL(TableExpression table) { this.table = Objects.requireNonNull(table); } - public SqlTable table() { + public TableExpression table() { return table; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java index a6990cba6..16204f999 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java @@ -27,6 +27,7 @@ import org.mybatis.dynamic.sql.SortSpecification; import org.mybatis.dynamic.sql.SqlCriterion; import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.TableExpression; import org.mybatis.dynamic.sql.VisitableCondition; import org.mybatis.dynamic.sql.select.join.JoinCondition; import org.mybatis.dynamic.sql.select.join.JoinCriterion; @@ -47,17 +48,17 @@ public class QueryExpressionDSL extends AbstractQueryExpressionDSL fromGatherer) { - super(fromGatherer.table); + QueryExpressionDSL(FromGatherer fromGatherer, TableExpression table) { + super(table); connector = fromGatherer.connector; selectList = fromGatherer.selectList; isDistinct = fromGatherer.isDistinct; selectDSL = Objects.requireNonNull(fromGatherer.selectDSL); } - QueryExpressionDSL(FromGatherer fromGatherer, String tableAlias) { - this(fromGatherer); - tableAliases.put(fromGatherer.table, tableAlias); + QueryExpressionDSL(FromGatherer fromGatherer, SqlTable table, String tableAlias) { + this(fromGatherer, table); + tableAliases.put(table, tableAlias); } public QueryExpressionWhereBuilder where() { @@ -176,7 +177,6 @@ public static class FromGatherer { private final List selectList; private final SelectDSL selectDSL; private final boolean isDistinct; - private SqlTable table; public FromGatherer(Builder builder) { this.connector = builder.connector; @@ -185,14 +185,33 @@ public FromGatherer(Builder builder) { this.isDistinct = builder.isDistinct; } + public QueryExpressionDSL from(Buildable select) { + return selectDSL.newQueryExpression(this, buildSubQuery(select)); + } + + public QueryExpressionDSL from(Buildable select, String tableAlias) { + return selectDSL.newQueryExpression(this, buildSubQuery(select, tableAlias)); + } + public QueryExpressionDSL from(SqlTable table) { - this.table = table; - return selectDSL.newQueryExpression(this); + return selectDSL.newQueryExpression(this, table); } public QueryExpressionDSL from(SqlTable table, String tableAlias) { - this.table = table; - return selectDSL.newQueryExpression(this, tableAlias); + return selectDSL.newQueryExpression(this, table, tableAlias); + } + + private SubQuery buildSubQuery(Buildable selectModel) { + return new SubQuery.Builder() + .withSelectModel(selectModel.build()) + .build(); + } + + private SubQuery buildSubQuery(Buildable selectModel, String alias) { + return new SubQuery.Builder() + .withSelectModel(selectModel.build()) + .withAlias(alias) + .build(); } public static class Builder { diff --git a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionModel.java b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionModel.java index 4bb7cda3c..6142792ec 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionModel.java @@ -15,8 +15,6 @@ */ package org.mybatis.dynamic.sql.select; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -28,6 +26,7 @@ import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.TableExpression; import org.mybatis.dynamic.sql.render.GuaranteedTableAliasCalculator; import org.mybatis.dynamic.sql.render.TableAliasCalculator; import org.mybatis.dynamic.sql.select.join.JoinModel; @@ -37,7 +36,7 @@ public class QueryExpressionModel { private final String connector; private final boolean isDistinct; private final List selectList; - private final SqlTable table; + private final TableExpression table; private final JoinModel joinModel; private final TableAliasCalculator tableAliasCalculator; private final WhereModel whereModel; @@ -67,7 +66,7 @@ public Stream mapColumns(Function mapper) { return selectList.stream().map(mapper); } - public SqlTable table() { + public TableExpression table() { return table; } @@ -87,11 +86,6 @@ public Optional groupByModel() { return Optional.ofNullable(groupByModel); } - public String calculateTableNameIncludingAlias(SqlTable table) { - return table.tableNameAtRuntime() - + spaceBefore(tableAliasCalculator.aliasForTable(table)); - } - public static Builder withSelectList(List columnList) { return new Builder().withSelectList(columnList); } @@ -100,7 +94,7 @@ public static class Builder { private String connector; private boolean isDistinct; private final List selectList = new ArrayList<>(); - private SqlTable table; + private TableExpression table; private final Map tableAliases = new HashMap<>(); private WhereModel whereModel; private JoinModel joinModel; @@ -111,7 +105,7 @@ public Builder withConnector(String connector) { return this; } - public Builder withTable(SqlTable table) { + public Builder withTable(TableExpression table) { this.table = table; return this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java index 69ba8d84c..5627734d0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java @@ -26,6 +26,8 @@ import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.SortSpecification; +import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.TableExpression; import org.mybatis.dynamic.sql.select.QueryExpressionDSL.FromGatherer; import org.mybatis.dynamic.sql.util.Buildable; @@ -92,14 +94,14 @@ public static QueryExpressionDSL.FromGatherer selectDistinct(Function newQueryExpression(FromGatherer fromGatherer) { - QueryExpressionDSL queryExpression = new QueryExpressionDSL<>(fromGatherer); + QueryExpressionDSL newQueryExpression(FromGatherer fromGatherer, TableExpression table) { + QueryExpressionDSL queryExpression = new QueryExpressionDSL<>(fromGatherer, table); queryExpressions.add(queryExpression); return queryExpression; } - QueryExpressionDSL newQueryExpression(FromGatherer fromGatherer, String tableAlias) { - QueryExpressionDSL queryExpression = new QueryExpressionDSL<>(fromGatherer, tableAlias); + QueryExpressionDSL newQueryExpression(FromGatherer fromGatherer, SqlTable table, String tableAlias) { + QueryExpressionDSL queryExpression = new QueryExpressionDSL<>(fromGatherer, table, tableAlias); queryExpressions.add(queryExpression); return queryExpression; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SubQuery.java b/src/main/java/org/mybatis/dynamic/sql/select/SubQuery.java new file mode 100644 index 000000000..d22185c72 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/SubQuery.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql.select; + +import java.util.Objects; +import java.util.Optional; + +import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.TableExpressionVisitor; + +public class SubQuery implements TableExpression { + private final SelectModel selectModel; + private final String alias; + + private SubQuery(Builder builder) { + selectModel = Objects.requireNonNull(builder.selectModel); + alias = builder.alias; + } + + public SelectModel selectModel() { + return selectModel; + } + + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public R accept(TableExpressionVisitor visitor) { + return visitor.visit(this); + } + + public static class Builder { + private SelectModel selectModel; + private String alias; + + public Builder withSelectModel(SelectModel selectModel) { + this.selectModel = selectModel; + return this; + } + + public Builder withAlias(String alias) { + this.alias = alias; + return this; + } + + public SubQuery build() { + return new SubQuery(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java index 1bed06a1d..eee86a7ef 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java @@ -21,11 +21,11 @@ import java.util.function.Function; import java.util.stream.Stream; -import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.TableExpression; public class JoinSpecification { - private final SqlTable table; + private final TableExpression table; private final List joinCriteria; private final JoinType joinType; @@ -35,7 +35,7 @@ private JoinSpecification(Builder builder) { joinType = Objects.requireNonNull(builder.joinType); } - public SqlTable table() { + public TableExpression table() { return table; } @@ -47,16 +47,16 @@ public JoinType joinType() { return joinType; } - public static Builder withJoinTable(SqlTable table) { + public static Builder withJoinTable(TableExpression table) { return new Builder().withJoinTable(table); } public static class Builder { - private SqlTable table; + private TableExpression table; private final List joinCriteria = new ArrayList<>(); private JoinType joinType; - public Builder withJoinTable(SqlTable table) { + public Builder withJoinTable(TableExpression table) { this.table = table; return this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java index a0e62f489..665530872 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java @@ -26,26 +26,40 @@ import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.select.join.JoinModel; import org.mybatis.dynamic.sql.select.join.JoinSpecification; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; public class JoinRenderer { private final JoinModel joinModel; private final QueryExpressionModel queryExpression; + private final TableExpressionRenderer tableExpressionRenderer; private JoinRenderer(Builder builder) { joinModel = Objects.requireNonNull(builder.joinModel); queryExpression = Objects.requireNonNull(builder.queryExpression); + tableExpressionRenderer = Objects.requireNonNull(builder.tableExpressionRenderer); } - public String render() { - return joinModel.mapJoinSpecifications(this::toRenderedString) - .collect(Collectors.joining(" ")); //$NON-NLS-1$ + public FragmentAndParameters render() { + FragmentCollector fc = joinModel.mapJoinSpecifications(this::renderJoinSpecification) + .collect(FragmentCollector.collect()); + + return FragmentAndParameters.withFragment(fc.fragments().collect(Collectors.joining(" "))) //$NON-NLS-1$ + .withParameters(fc.parameters()) + .build(); } - private String toRenderedString(JoinSpecification joinSpecification) { - return spaceAfter(joinSpecification.joinType().shortType()) + private FragmentAndParameters renderJoinSpecification(JoinSpecification joinSpecification) { + FragmentAndParameters renderedTable = joinSpecification.table().accept(tableExpressionRenderer); + + String fragment = spaceAfter(joinSpecification.joinType().shortType()) + "join" //$NON-NLS-1$ - + spaceBefore(queryExpression.calculateTableNameIncludingAlias(joinSpecification.table())) + + spaceBefore(renderedTable.fragment()) + spaceBefore(renderConditions(joinSpecification)); + + return FragmentAndParameters.withFragment(fragment) + .withParameters(renderedTable.parameters()) + .build(); } private String renderConditions(JoinSpecification joinSpecification) { @@ -71,6 +85,7 @@ public static Builder withJoinModel(JoinModel joinModel) { public static class Builder { private JoinModel joinModel; private QueryExpressionModel queryExpression; + private TableExpressionRenderer tableExpressionRenderer; public Builder withJoinModel(JoinModel joinModel) { this.joinModel = joinModel; @@ -82,6 +97,11 @@ public Builder withQueryExpression(QueryExpressionModel queryExpression) { return this; } + public Builder withTableExpressionRenderer(TableExpressionRenderer tableExpressionRenderer) { + this.tableExpressionRenderer = tableExpressionRenderer; + return this; + } + public JoinRenderer build() { return new JoinRenderer(this); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/QueryExpressionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/QueryExpressionRenderer.java index 742f4ce68..21d1c3db8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/QueryExpressionRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/QueryExpressionRenderer.java @@ -24,7 +24,7 @@ import java.util.stream.Collectors; import org.mybatis.dynamic.sql.BasicColumn; -import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.TableExpression; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.select.GroupByModel; import org.mybatis.dynamic.sql.select.QueryExpressionModel; @@ -39,50 +39,40 @@ public class QueryExpressionRenderer { private final QueryExpressionModel queryExpression; private final RenderingStrategy renderingStrategy; private final AtomicInteger sequence; + private final TableExpressionRenderer tableExpressionRenderer; private QueryExpressionRenderer(Builder builder) { queryExpression = Objects.requireNonNull(builder.queryExpression); renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); sequence = Objects.requireNonNull(builder.sequence); - } - - public FragmentAndParameters render() { - return queryExpression.whereModel() - .flatMap(this::renderWhereClause) - .map(this::renderWithWhereClause) - .orElseGet(this::renderWithoutWhereClause); - } - - private FragmentAndParameters renderWithWhereClause(WhereClauseProvider whereClause) { - return FragmentAndParameters.withFragment(calculateQueryExpression(whereClause)) - .withParameters(whereClause.getParameters()) - .build(); - } - - private FragmentAndParameters renderWithoutWhereClause() { - return FragmentAndParameters.withFragment(calculateQueryExpression()) + tableExpressionRenderer = new TableExpressionRenderer.Builder() + .withTableAliasCalculator(queryExpression.tableAliasCalculator()) + .withRenderingStrategy(renderingStrategy) + .withSequence(sequence) .build(); } - private String calculateQueryExpression() { - return calculateQueryExpressionStart() - + spaceBefore(queryExpression.groupByModel().map(this::renderGroupBy)); - } - - private String calculateQueryExpression(WhereClauseProvider whereClause) { - return calculateQueryExpressionStart() - + spaceBefore(whereClause.getWhereClause()) - + spaceBefore(queryExpression.groupByModel().map(this::renderGroupBy)); + public FragmentAndParameters render() { + FragmentAndParameters answer = calculateQueryExpressionStart(); + answer = addJoinClause(answer); + answer = addWhereClause(answer); + answer = addGroupByClause(answer); + return answer; } - private String calculateQueryExpressionStart() { - return spaceAfter(queryExpression.connector()) + private FragmentAndParameters calculateQueryExpressionStart() { + String start = spaceAfter(queryExpression.connector()) + "select " //$NON-NLS-1$ + (queryExpression.isDistinct() ? "distinct " : "") //$NON-NLS-1$ //$NON-NLS-2$ + calculateColumnList() - + " from " //$NON-NLS-1$ - + calculateTableName(queryExpression.table()) - + spaceBefore(queryExpression.joinModel().map(this::renderJoin)); + + " from "; //$NON-NLS-1$ + + FragmentAndParameters renderedTable = renderTableExpression(queryExpression.table()); + start += renderedTable.fragment(); + + return FragmentAndParameters.withFragment(start) + .withParameters(renderedTable.parameters()) + .build(); } private String calculateColumnList() { @@ -90,21 +80,36 @@ private String calculateColumnList() { .collect(Collectors.joining(", ")); //$NON-NLS-1$ } - private String calculateTableName(SqlTable table) { - return queryExpression.calculateTableNameIncludingAlias(table); - } - private String applyTableAndColumnAlias(BasicColumn selectListItem) { return selectListItem.renderWithTableAndColumnAlias(queryExpression.tableAliasCalculator()); } - private String renderJoin(JoinModel joinModel) { + private FragmentAndParameters renderTableExpression(TableExpression table) { + return table.accept(tableExpressionRenderer); + } + + private FragmentAndParameters addJoinClause(FragmentAndParameters partial) { + return queryExpression.joinModel() + .map(this::renderJoin) + .map(fp -> partial.add(spaceBefore(fp.fragment()), fp.parameters())) + .orElse(partial); + } + + private FragmentAndParameters renderJoin(JoinModel joinModel) { return JoinRenderer.withJoinModel(joinModel) .withQueryExpression(queryExpression) + .withTableExpressionRenderer(tableExpressionRenderer) .build() .render(); } + private FragmentAndParameters addWhereClause(FragmentAndParameters partial) { + return queryExpression.whereModel() + .flatMap(this::renderWhereClause) + .map(wc -> partial.add(spaceBefore(wc.getWhereClause()), wc.getParameters())) + .orElse(partial); + } + private Optional renderWhereClause(WhereModel whereModel) { return WhereRenderer.withWhereModel(whereModel) .withRenderingStrategy(renderingStrategy) @@ -114,6 +119,13 @@ private Optional renderWhereClause(WhereModel whereModel) { .render(); } + private FragmentAndParameters addGroupByClause(FragmentAndParameters partial) { + return queryExpression.groupByModel() + .map(this::renderGroupBy) + .map(s -> partial.add(spaceBefore(s))) + .orElse(partial); + } + private String renderGroupBy(GroupByModel groupByModel) { return groupByModel.mapColumns(this::applyTableAlias) .collect(CustomCollectors.joining(", ", "group by ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/TableExpressionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/TableExpressionRenderer.java new file mode 100644 index 000000000..56081fc2f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/TableExpressionRenderer.java @@ -0,0 +1,98 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql.select.render; + +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import org.mybatis.dynamic.sql.SqlTable; +import org.mybatis.dynamic.sql.TableExpressionVisitor; +import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.render.TableAliasCalculator; +import org.mybatis.dynamic.sql.select.SubQuery; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class TableExpressionRenderer implements TableExpressionVisitor { + private final TableAliasCalculator tableAliasCalculator; + private final RenderingStrategy renderingStrategy; + private final AtomicInteger sequence; + + private TableExpressionRenderer(Builder builder) { + tableAliasCalculator = Objects.requireNonNull(builder.tableAliasCalculator); + renderingStrategy = Objects.requireNonNull(builder.renderingStrategy); + sequence = Objects.requireNonNull(builder.sequence); + } + + @Override + public FragmentAndParameters visit(SqlTable table) { + return FragmentAndParameters.withFragment( + tableAliasCalculator.aliasForTable(table) + .map(a -> table.tableNameAtRuntime() + spaceBefore(a)) + .orElseGet(table::tableNameAtRuntime)) + .build(); + } + + @Override + public FragmentAndParameters visit(SubQuery subQuery) { + SelectStatementProvider selectStatement = new SelectRenderer.Builder() + .withSelectModel(subQuery.selectModel()) + .withRenderingStrategy(renderingStrategy) + .withSequence(sequence) + .build() + .render(); + + String fragment = "(" + selectStatement.getSelectStatement() + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + + fragment = applyAlias(fragment, subQuery); + + return FragmentAndParameters.withFragment(fragment) + .withParameters(selectStatement.getParameters()) + .build(); + } + + private String applyAlias(String fragment, SubQuery subQuery) { + return subQuery.alias() + .map(a -> fragment + spaceBefore(a)) + .orElse(fragment); + } + + public static class Builder { + private TableAliasCalculator tableAliasCalculator; + private RenderingStrategy renderingStrategy; + private AtomicInteger sequence; + + public Builder withTableAliasCalculator(TableAliasCalculator tableAliasCalculator) { + this.tableAliasCalculator = tableAliasCalculator; + return this; + } + + public Builder withRenderingStrategy(RenderingStrategy renderingStrategy) { + this.renderingStrategy = renderingStrategy; + return this; + } + + public Builder withSequence(AtomicInteger sequence) { + this.sequence = sequence; + return this; + } + + public TableExpressionRenderer build() { + return new TableExpressionRenderer(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/FragmentAndParameters.java b/src/main/java/org/mybatis/dynamic/sql/util/FragmentAndParameters.java index 3431d12ce..0e5596ef6 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/FragmentAndParameters.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/FragmentAndParameters.java @@ -38,6 +38,19 @@ public Map parameters() { return parameters; } + public FragmentAndParameters add(String newFragment) { + return withFragment(fragment + newFragment) + .withParameters(parameters) + .build(); + } + + public FragmentAndParameters add(String newFragment, Map newParameters) { + return withFragment(fragment + newFragment) + .withParameters(parameters) + .withParameters(newParameters) + .build(); + } + public static Builder withFragment(String fragment) { return new Builder().withFragment(fragment); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWithSubselect.java index dcee338d2..fb61aebf8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsEqualToWithSubselect.java @@ -15,6 +15,7 @@ */ package org.mybatis.dynamic.sql.where.condition; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.AbstractSubselectCondition; import org.mybatis.dynamic.sql.select.SelectModel; import org.mybatis.dynamic.sql.util.Buildable; @@ -25,6 +26,7 @@ protected IsEqualToWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } + @NotNull public static IsEqualToWithSubselect of(Buildable selectModelBuilder) { return new IsEqualToWithSubselect<>(selectModelBuilder); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWithSubselect.java index e6d2b3155..7d2bb0da1 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanOrEqualToWithSubselect.java @@ -15,6 +15,7 @@ */ package org.mybatis.dynamic.sql.where.condition; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.AbstractSubselectCondition; import org.mybatis.dynamic.sql.select.SelectModel; import org.mybatis.dynamic.sql.util.Buildable; @@ -25,6 +26,7 @@ protected IsGreaterThanOrEqualToWithSubselect(Buildable selectModel super(selectModelBuilder); } + @NotNull public static IsGreaterThanOrEqualToWithSubselect of(Buildable selectModelBuilder) { return new IsGreaterThanOrEqualToWithSubselect<>(selectModelBuilder); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWithSubselect.java index c7792e63a..28be24d29 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsGreaterThanWithSubselect.java @@ -15,6 +15,7 @@ */ package org.mybatis.dynamic.sql.where.condition; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.AbstractSubselectCondition; import org.mybatis.dynamic.sql.select.SelectModel; import org.mybatis.dynamic.sql.util.Buildable; @@ -25,6 +26,7 @@ protected IsGreaterThanWithSubselect(Buildable selectModelBuilder) super(selectModelBuilder); } + @NotNull public static IsGreaterThanWithSubselect of(Buildable selectModelBuilder) { return new IsGreaterThanWithSubselect<>(selectModelBuilder); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWithSubselect.java index 4f7971beb..5593bef97 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInWithSubselect.java @@ -15,6 +15,7 @@ */ package org.mybatis.dynamic.sql.where.condition; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.AbstractSubselectCondition; import org.mybatis.dynamic.sql.select.SelectModel; import org.mybatis.dynamic.sql.util.Buildable; @@ -25,6 +26,7 @@ protected IsInWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } + @NotNull public static IsInWithSubselect of(Buildable selectModelBuilder) { return new IsInWithSubselect<>(selectModelBuilder); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWithSubselect.java index 9c798f7a6..f2f4e81e3 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanOrEqualToWithSubselect.java @@ -15,6 +15,7 @@ */ package org.mybatis.dynamic.sql.where.condition; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.AbstractSubselectCondition; import org.mybatis.dynamic.sql.select.SelectModel; import org.mybatis.dynamic.sql.util.Buildable; @@ -25,6 +26,7 @@ protected IsLessThanOrEqualToWithSubselect(Buildable selectModelBui super(selectModelBuilder); } + @NotNull public static IsLessThanOrEqualToWithSubselect of(Buildable selectModelBuilder) { return new IsLessThanOrEqualToWithSubselect<>(selectModelBuilder); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWithSubselect.java index ed000b702..285652d3e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLessThanWithSubselect.java @@ -15,6 +15,7 @@ */ package org.mybatis.dynamic.sql.where.condition; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.AbstractSubselectCondition; import org.mybatis.dynamic.sql.select.SelectModel; import org.mybatis.dynamic.sql.util.Buildable; @@ -25,6 +26,7 @@ protected IsLessThanWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } + @NotNull public static IsLessThanWithSubselect of(Buildable selectModelBuilder) { return new IsLessThanWithSubselect<>(selectModelBuilder); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWithSubselect.java index 4c3915e4f..1e1d4ffd5 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotEqualToWithSubselect.java @@ -15,6 +15,7 @@ */ package org.mybatis.dynamic.sql.where.condition; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.AbstractSubselectCondition; import org.mybatis.dynamic.sql.select.SelectModel; import org.mybatis.dynamic.sql.util.Buildable; @@ -25,6 +26,7 @@ protected IsNotEqualToWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } + @NotNull public static IsNotEqualToWithSubselect of(Buildable selectModelBuilder) { return new IsNotEqualToWithSubselect<>(selectModelBuilder); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWithSubselect.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWithSubselect.java index 2c72ffe18..848691a67 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWithSubselect.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInWithSubselect.java @@ -15,6 +15,7 @@ */ package org.mybatis.dynamic.sql.where.condition; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.AbstractSubselectCondition; import org.mybatis.dynamic.sql.select.SelectModel; import org.mybatis.dynamic.sql.util.Buildable; @@ -25,6 +26,7 @@ protected IsNotInWithSubselect(Buildable selectModelBuilder) { super(selectModelBuilder); } + @NotNull public static IsNotInWithSubselect of(Buildable selectModelBuilder) { return new IsNotInWithSubselect<>(selectModelBuilder); } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinConditions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinConditions.kt new file mode 100644 index 000000000..012dab612 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinConditions.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.where.condition.IsEqualToWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsGreaterThanOrEqualToWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsGreaterThanWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsInWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsLessThanOrEqualToWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsLessThanWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsNotEqualToWithSubselect +import org.mybatis.dynamic.sql.where.condition.IsNotInWithSubselect + +fun isEqualTo(subQuery: KotlinSubQueryBuilder.() -> KotlinSubQueryBuilder) = + IsEqualToWithSubselect.of(subQuery(KotlinSubQueryBuilder()).selectBuilder) + +fun isNotEqualTo(subQuery: KotlinSubQueryBuilder.() -> KotlinSubQueryBuilder) = + IsNotEqualToWithSubselect.of(subQuery(KotlinSubQueryBuilder()).selectBuilder) + +fun isIn(subQuery: KotlinSubQueryBuilder.() -> KotlinSubQueryBuilder) = + IsInWithSubselect.of(subQuery(KotlinSubQueryBuilder()).selectBuilder) + +fun isNotIn(subQuery: KotlinSubQueryBuilder.() -> KotlinSubQueryBuilder) = + IsNotInWithSubselect.of(subQuery(KotlinSubQueryBuilder()).selectBuilder) + +fun isGreaterThan(subQuery: KotlinSubQueryBuilder.() -> KotlinSubQueryBuilder) = + IsGreaterThanWithSubselect.of(subQuery(KotlinSubQueryBuilder()).selectBuilder) + +fun isGreaterThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> KotlinSubQueryBuilder) = + IsGreaterThanOrEqualToWithSubselect.of(subQuery(KotlinSubQueryBuilder()).selectBuilder) + +fun isLessThan(subQuery: KotlinSubQueryBuilder.() -> KotlinSubQueryBuilder) = + IsLessThanWithSubselect.of(subQuery(KotlinSubQueryBuilder()).selectBuilder) + +fun isLessThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> KotlinSubQueryBuilder) = + IsLessThanOrEqualToWithSubselect.of(subQuery(KotlinSubQueryBuilder()).selectBuilder) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertHelpers.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertHelpers.kt index 62e18e779..4746073c6 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertHelpers.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinInsertHelpers.kt @@ -32,3 +32,5 @@ typealias InsertCompleter = @MyBatisDslMarker InsertDSL.() -> Buildable = @MyBatisDslMarker MultiRowInsertDSL.() -> Buildable> typealias BatchInsertCompleter = @MyBatisDslMarker BatchInsertDSL.() -> Buildable> + +typealias InsertSelectCompleter = @MyBatisDslMarker KotlinInsertSelectSubQueryBuilder.() -> KotlinInsertSelectSubQueryBuilder diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt index f7f4ece19..3b5eecdde 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt @@ -17,7 +17,6 @@ package org.mybatis.dynamic.sql.util.kotlin import org.mybatis.dynamic.sql.BasicColumn import org.mybatis.dynamic.sql.SortSpecification -import org.mybatis.dynamic.sql.SqlBuilder import org.mybatis.dynamic.sql.SqlTable import org.mybatis.dynamic.sql.select.SelectModel import org.mybatis.dynamic.sql.util.Buildable @@ -25,19 +24,6 @@ import org.mybatis.dynamic.sql.select.QueryExpressionDSL typealias SelectCompleter = KotlinSelectBuilder.() -> KotlinSelectBuilder -// convenience methods for building partials and sub-queries -fun select(vararg basicColumns: BasicColumn, complete: SelectCompleter) = - select(basicColumns.asList(), complete) - -fun select(basicColumns: List, complete: SelectCompleter) = - complete(KotlinSelectBuilder(SqlBuilder.select(basicColumns))) - -fun selectDistinct(vararg basicColumns: BasicColumn, complete: SelectCompleter) = - selectDistinct(basicColumns.asList(), complete) - -fun selectDistinct(basicColumns: List, complete: SelectCompleter) = - complete(KotlinSelectBuilder(SqlBuilder.selectDistinct(basicColumns))) - @Suppress("TooManyFunctions") class KotlinSelectBuilder(private val fromGatherer: QueryExpressionDSL.FromGatherer) : KotlinBaseJoiningBuilder, @@ -56,6 +42,12 @@ class KotlinSelectBuilder(private val fromGatherer: QueryExpressionDSL.FromGathe dsl = fromGatherer.from(table, alias) } + fun from(subQuery: KotlinQualifiedSubQueryBuilder.() -> KotlinQualifiedSubQueryBuilder) = + apply { + val builder = subQuery(KotlinQualifiedSubQueryBuilder()) + dsl = fromGatherer.from(builder.selectBuilder, builder.correlationName) + } + fun groupBy(vararg columns: BasicColumn) = apply { getDsl().groupBy(columns.toList()) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSubQueryBuilders.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSubQueryBuilders.kt new file mode 100644 index 000000000..2c0d76de1 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSubQueryBuilders.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql.util.kotlin + +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.SqlColumn + +@MyBatisDslMarker +sealed class KotlinBaseSubQueryBuilder > { + lateinit var selectBuilder: KotlinSelectBuilder + + fun select(vararg selectList: BasicColumn, completer: SelectCompleter) = + select(selectList.toList(), completer) + + fun select(selectList: List, completer: SelectCompleter) = + applySelf { + selectBuilder = completer(KotlinSelectBuilder(SqlBuilder.select(selectList))) + } + + fun selectDistinct(vararg selectList: BasicColumn, completer: SelectCompleter) = + selectDistinct(selectList.toList(), completer) + + fun selectDistinct(selectList: List, completer: SelectCompleter) = + applySelf { + selectBuilder = completer(KotlinSelectBuilder(SqlBuilder.selectDistinct(selectList))) + } + + private fun applySelf(block: T.() -> Unit): T = + self().apply { block() } + + protected abstract fun self(): T +} + +class KotlinSubQueryBuilder: KotlinBaseSubQueryBuilder() { + override fun self() = this +} + +class KotlinQualifiedSubQueryBuilder: KotlinBaseSubQueryBuilder() { + var correlationName: String? = null + + operator fun String.unaryPlus(): KotlinQualifiedSubQueryBuilder { + correlationName = this + return self() + } + + override fun self() = this +} + +class KotlinInsertSelectSubQueryBuilder : KotlinBaseSubQueryBuilder() { + lateinit var columnList : List> + + fun columns(vararg columnList : SqlColumn<*>) = + columns(columnList.asList()) + + fun columns(columnList : List>) = + apply { + this.columnList = columnList + } + + override fun self() = this +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/MapperSupportFunctions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/MapperSupportFunctions.kt index 723814ed7..bd64909bd 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/MapperSupportFunctions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/MapperSupportFunctions.kt @@ -21,6 +21,7 @@ import org.mybatis.dynamic.sql.SqlBuilder import org.mybatis.dynamic.sql.SqlTable import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider +import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider import org.mybatis.dynamic.sql.select.render.SelectStatementProvider @@ -29,6 +30,7 @@ import org.mybatis.dynamic.sql.util.kotlin.CountCompleter import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter import org.mybatis.dynamic.sql.util.kotlin.InsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.InsertSelectCompleter import org.mybatis.dynamic.sql.util.kotlin.KotlinCountBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinSelectBuilder import org.mybatis.dynamic.sql.util.kotlin.MultiRowInsertCompleter @@ -66,6 +68,13 @@ fun insertMultiple( ) = mapper(SqlBuilder.insertMultiple(records).into(table, completer)) +fun insertSelect( + mapper: (InsertSelectStatementProvider) -> Int, + table: SqlTable, + completer: InsertSelectCompleter +) = + mapper(insertSelect(table, completer)) + fun selectDistinct( mapper: (SelectStatementProvider) -> List, selectList: List, diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt index edea07934..90b04435d 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt @@ -28,9 +28,11 @@ import org.mybatis.dynamic.sql.util.kotlin.CountCompleter import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter import org.mybatis.dynamic.sql.util.kotlin.InsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.InsertSelectCompleter import org.mybatis.dynamic.sql.util.kotlin.KotlinCountBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinCountColumnBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinDeleteBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinInsertSelectSubQueryBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinSelectBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinUpdateBuilder import org.mybatis.dynamic.sql.util.kotlin.MultiRowInsertCompleter @@ -59,6 +61,14 @@ fun insertInto(table: SqlTable, completer: GeneralInsertCompleter) = completer(GeneralInsertDSL.insertInto(table)) .build().render(RenderingStrategies.MYBATIS3) +fun insertSelect(table: SqlTable, completer: InsertSelectCompleter) = + with(completer(KotlinInsertSelectSubQueryBuilder())) { + SqlBuilder.insertInto(table) + .withColumnList(columnList) + .withSelectStatement(selectBuilder) + .build().render(RenderingStrategies.MYBATIS3) + } + fun InsertDSL.IntoGatherer.into(table: SqlTable, completer: InsertCompleter) = completer(into(table)).build().render(RenderingStrategies.MYBATIS3) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt index fe9872b49..cb92f6dd2 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt @@ -23,6 +23,7 @@ import org.mybatis.dynamic.sql.SqlTable import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider import org.mybatis.dynamic.sql.insert.render.BatchInsert import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider +import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider import org.mybatis.dynamic.sql.select.render.SelectStatementProvider @@ -33,6 +34,7 @@ import org.mybatis.dynamic.sql.util.kotlin.CountCompleter import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter import org.mybatis.dynamic.sql.util.kotlin.InsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.InsertSelectCompleter import org.mybatis.dynamic.sql.util.kotlin.MultiRowInsertCompleter import org.mybatis.dynamic.sql.util.kotlin.MyBatisDslMarker import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter @@ -109,6 +111,12 @@ fun NamedParameterJdbcTemplate.insertMultiple( ) = update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement), keyHolder) +fun NamedParameterJdbcTemplate.insertSelect(table: SqlTable, completer: InsertSelectCompleter) = + insertSelect(org.mybatis.dynamic.sql.util.kotlin.spring.insertSelect(table, completer)) + +fun NamedParameterJdbcTemplate.insertSelect(insertStatement: InsertSelectStatementProvider) = + update(insertStatement.insertStatement, MapSqlParameterSource(insertStatement.parameters)) + // insert with KeyHolder support fun NamedParameterJdbcTemplate.withKeyHolder(keyHolder: KeyHolder, build: KeyHolderHelper.() -> Int) = build(KeyHolderHelper(keyHolder, this)) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt index 3bb942e78..268c6f2d1 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt @@ -23,6 +23,7 @@ import org.mybatis.dynamic.sql.insert.BatchInsertDSL import org.mybatis.dynamic.sql.insert.GeneralInsertDSL import org.mybatis.dynamic.sql.insert.InsertDSL import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL +import org.mybatis.dynamic.sql.insert.render.InsertSelectStatementProvider import org.mybatis.dynamic.sql.render.RenderingStrategies import org.mybatis.dynamic.sql.util.kotlin.BatchInsertCompleter import org.mybatis.dynamic.sql.util.kotlin.CountColumnCompleter @@ -30,9 +31,11 @@ import org.mybatis.dynamic.sql.util.kotlin.CountCompleter import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter import org.mybatis.dynamic.sql.util.kotlin.InsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.InsertSelectCompleter import org.mybatis.dynamic.sql.util.kotlin.KotlinCountBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinCountColumnBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinDeleteBuilder +import org.mybatis.dynamic.sql.util.kotlin.KotlinInsertSelectSubQueryBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinSelectBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinUpdateBuilder import org.mybatis.dynamic.sql.util.kotlin.MultiRowInsertCompleter @@ -61,6 +64,14 @@ fun insertInto(table: SqlTable, completer: GeneralInsertCompleter) = completer(GeneralInsertDSL.insertInto(table)) .build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) +fun insertSelect(table: SqlTable, completer: InsertSelectCompleter) = + with(completer(KotlinInsertSelectSubQueryBuilder())) { + SqlBuilder.insertInto(table) + .withColumnList(columnList) + .withSelectStatement(selectBuilder) + .build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) + } + fun BatchInsertDSL.IntoGatherer.into(table: SqlTable, completer: BatchInsertCompleter) = completer(into(table)).build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) diff --git a/src/site/markdown/docs/subQueries.md b/src/site/markdown/docs/subQueries.md new file mode 100644 index 000000000..61612b11c --- /dev/null +++ b/src/site/markdown/docs/subQueries.md @@ -0,0 +1,248 @@ +# SubQuery Support +The library currently supports subQueries in the following areas: + +1. In certain where conditions +1. In certain insert statements +1. In the "from" clause of a select statement + +## SubQueries in Where Conditions +The library support subQueries in the following where conditions: + +- isEqualTo +- isNotEqualTo +- isIn +- isNotIn +- isGreaterThan +- isGreaterThanOrEqualTo +- isLessThan +- isLessThanOrEqualTo + +A Java example is as follows: + +```java +SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .where(brainWeight, isEqualTo( + select(min(brainWeight)) + .from(animalData) + ) + ) + .orderBy(animalName) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +### Kotlin Support +The library includes Kotlin versions of the where conditions that allow use of the Kotlin subQuery builder. The Kotlin +where conditions are in the `org.mybatis.dynamic.sql.util.kotlin` package. An example is as follows: + +```kotlin +val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isEqualTo { + select(max(id)) { + from(Person) + } + }) +} +``` + +## SubQueries in Insert Statements +The library supports an INSERT statement that retrieves values from a SELECT statement. For example: + +```java +InsertSelectStatementProvider insertSelectStatement = insertInto(animalDataCopy) + .withColumnList(id, animalName, bodyWeight, brainWeight) + .withSelectStatement( + select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .where(id, isLessThan(22)) + ) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +### Kotlin Support + +The library includes a Kotlin builder for subQueries in insert statements that integrates with the select DSL. You +can write inserts like this: + +```kotlin +val insertStatement = insertSelect(Person) { + columns(id, firstName, lastName, birthDate, employed, occupation, addressId) + select(add(id, constant("100")), firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + orderBy(id) + } +} +``` + +## SubQueries in a From Clause + +The library supports subQueries in from clauses and the syntax is a natural extension of the +select DSL. An example is as follows: + +```java +DerivedColumn rowNum = DerivedColumn.of("rownum()"); + +SelectStatementProvider selectStatement = + select(animalName, rowNum) + .from( + select(id, animalName) + .from(animalData) + .where(id, isLessThan(22)) + .orderBy(animalName.descending()) + ) + .where(rowNum, isLessThan(5)) + .and(animalName, isLike("%a%")) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +Notice the use of a `DerivedColumn` to easily specify a function like `rownum()` that can be +used both in the select list and in a where condition. + +### Table Qualifiers with SubQueries + +The library attempts to automatically calculate table qualifiers. If a table qualifier is specified, +the library will automatically render the table qualifier on all columns associated with the +table. For example with the following query: + +```java +SelectStatementProvider selectStatement = + select(id, animalName) + .from(animalData, "ad") + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +The library will render SQL as: + +```sql +select ad.id, ad.animal_name +from AnimalData ad +``` + +Notice that the table qualifier `ad` is automatically applied to columns in the select list. + +In the case of join queries the table qualifier specified, or if not specified the table name +itself, will be used as the table qualifier. + +With subQueries, it is important to understand the limits of automatic table qualifiers. The rules are +as follows: + +1. The scope of automatic table qualifiers is limited to a single select statement. For subQueries, the outer + query has a different scope than the subQuery. +1. A qualifier can be applied to a subQuery as a whole, but that qualifier is not automatically applied to + any column + +As an example, consider the following query: + +```java +DerivedColumn rowNum = DerivedColumn.of("rownum()"); + +SelectStatementProvider selectStatement = + select(animalName, rowNum) + .from( + select(id, animalName) + .from(animalData, "a") + .where(id, isLessThan(22)) + .orderBy(animalName.descending()), + "b" + ) + .where(rowNum, isLessThan(5)) + .and(animalName, isLike("%a%")) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +The rendered SQL will be as follows: + +```sql +select animal_name, rownum() +from (select a.id, a.animal_name + from AnimalDate a + where id < #{parameters.p1} + order by animal_name desc) b +where rownum() < #{parameters.p2} + and animal_name like #{parameters.p3} +``` + +Notice that the qualifier `a` is automatically applied to columns in the subQuery and that the +qualifier `b` is not applied anywhere. + +If your query requires the subQuery qualifier to be applied to columns in the outer select list, +you can manually apply the qualifier to columns as follows: + +```java +DerivedColumn rowNum = DerivedColumn.of("rownum()"); + +SelectStatementProvider selectStatement = + select(animalName.qualifiedWith("b"), rowNum) + .from( + select(id, animalName) + .from(animalData, "a") + .where(id, isLessThan(22)) + .orderBy(animalName.descending()), + "b" + ) + .where(rowNum, isLessThan(5)) + .and(animalName.qualifiedWith("b"), isLike("%a%")) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +In this case, we have manually applied the qualifier `b` to columns in the outer query. The +rendered SQL looks like this: + +```sql +select b.animal_name, rownum() +from (select a.id, a.animal_name + from AnimalDate a + where id < #{parameters.p1} + order by animal_name desc) b +where rownum() < #{parameters.p2} + and b.animal_name like #{parameters.p3} +``` + +### Kotlin Support + +The library includes a Kotlin builder for subQueries that integrates with the select DSL. You +can write queries like this: + +```kotlin +val selectStatement = + select(firstName, rowNum) { + from { + select(id, firstName) { + from(Person) + where(id, isLessThan(22)) + orderBy(firstName.descending()) + } + } + where(rowNum, isLessThan(5)) + and(firstName, isLike("%a%")) + } +``` + +The same rules about table qualifiers apply as stated above. In Kotlin, a subQuery qualifier +can be added with the overloaded "+" operator as shown below: + +```kotlin +val selectStatement = + select(firstName, rowNum) { + from { + select(id, firstName) { + from(Person, "a") + where(id, isLessThan(22)) + orderBy(firstName.descending()) + } + + "b" + } + where(rowNum, isLessThan(5)) + and(firstName, isLike("%a%")) + } +``` + +In this case the `a` qualifier is used in the context of the inner select statement and +the `b` qualifier is applied to the subQuery as a whole. diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 1fa622d69..6cb33e241 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -1,7 +1,9 @@ # MyBatis Dynamic SQL -MyBatis Dynamic SQL is an SQL templating library that makes it easier to execute dynamic SQL with MyBatis. It generates SQL that is formatted in such a way that it can be directly executed by MyBatis. +MyBatis Dynamic SQL is an SQL DSL (domain specific language). It allows developers to write SQL in Java or Kotlin using the natural feel of native SQL. It also +includes many functions for creating very dynamic SQL statements based on current runtime parameter values. -The library also supports generating SQL that is formatted for use by Spring JDBC Templates. +The DSL will render standard SQL DELETE, INSERT, SELECT, and UPDATE statements - and associated +parameters - that can be used directly by SQL execution engines like MyBatis or Spring JDBC template. Please read the user's guide for detailed instructions on use. The user's guide is accessible through menu links to the left. diff --git a/src/site/site.xml b/src/site/site.xml index e9c1ba65a..a913ae305 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -47,6 +47,7 @@ + diff --git a/src/test/java/examples/animal/data/SubQueryTest.java b/src/test/java/examples/animal/data/SubQueryTest.java new file mode 100644 index 000000000..9e064958c --- /dev/null +++ b/src/test/java/examples/animal/data/SubQueryTest.java @@ -0,0 +1,221 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.animal.data; + +import static examples.animal.data.AnimalDataDynamicSqlSupport.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.sql.Connection; +import java.sql.DriverManager; +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.jdbc.ScriptRunner; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mybatis.dynamic.sql.DerivedColumn; +import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; + +class SubQueryTest { + private static final String JDBC_URL = "jdbc:hsqldb:mem:aname"; + private static final String JDBC_DRIVER = "org.hsqldb.jdbcDriver"; + + private SqlSessionFactory sqlSessionFactory; + + @BeforeEach + void setup() throws Exception { + Class.forName(JDBC_DRIVER); + InputStream is = getClass().getResourceAsStream("/examples/animal/data/CreateAnimalData.sql"); + try (Connection connection = DriverManager.getConnection(JDBC_URL, "sa", "")) { + ScriptRunner sr = new ScriptRunner(connection); + sr.setLogWriter(null); + sr.runScript(new InputStreamReader(is)); + } + + UnpooledDataSource ds = new UnpooledDataSource(JDBC_DRIVER, JDBC_URL, "sa", ""); + Environment environment = new Environment("test", new JdbcTransactionFactory(), ds); + Configuration config = new Configuration(environment); + config.addMapper(CommonSelectMapper.class); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(config); + } + + @Test + void testBasicSubQuery() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + DerivedColumn rowNum = DerivedColumn.of("rownum()"); + + SelectStatementProvider selectStatement = select(animalName, rowNum) + .from( + select(id, animalName) + .from(animalData) + .where(id, isLessThan(22)) + .orderBy(animalName.descending()) + ) + .where(rowNum, isLessThan(5)) + .and(animalName, isLike("%a%")) + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()).isEqualTo( + "select animal_name, rownum() " + + "from (select id, animal_name " + + "from AnimalData where id < #{parameters.p1,jdbcType=INTEGER} " + + "order by animal_name DESC) " + + "where rownum() < #{parameters.p2} and animal_name like #{parameters.p3,jdbcType=VARCHAR}" + ); + assertThat(selectStatement.getParameters()).containsEntry("p1", 22); + assertThat(selectStatement.getParameters()).containsEntry("p2", 5); + assertThat(selectStatement.getParameters()).containsEntry("p3", "%a%"); + + List> rows = mapper.selectManyMappedRows(selectStatement); + assertThat(rows).hasSize(4); + + assertThat(rows.get(2)).containsEntry("ANIMAL_NAME", "Chinchilla"); + assertThat(rows.get(2)).containsEntry("ROWNUM", 3); + } + } + + @Test + void testSimpleAliases() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + DerivedColumn rowNum = DerivedColumn.of("rownum()"); + + SelectStatementProvider selectStatement = select(animalName, rowNum) + .from( + select(id, animalName) + .from(animalData, "a") + .where(id, isLessThan(22)) + .orderBy(animalName.descending()), + "b" + ) + .where(rowNum, isLessThan(5)) + .and(animalName, isLike("%a%")) + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()).isEqualTo( + "select animal_name, rownum() " + + "from (select a.id, a.animal_name " + + "from AnimalData a where a.id < #{parameters.p1,jdbcType=INTEGER} " + + "order by animal_name DESC) b " + + "where rownum() < #{parameters.p2} and animal_name like #{parameters.p3,jdbcType=VARCHAR}" + ); + assertThat(selectStatement.getParameters()).containsEntry("p1", 22); + assertThat(selectStatement.getParameters()).containsEntry("p2", 5); + assertThat(selectStatement.getParameters()).containsEntry("p3", "%a%"); + + List> rows = mapper.selectManyMappedRows(selectStatement); + assertThat(rows).hasSize(4); + + assertThat(rows.get(2)).containsEntry("ANIMAL_NAME", "Chinchilla"); + assertThat(rows.get(2)).containsEntry("ROWNUM", 3); + } + } + + @Test + void testSimpleAliasesWithManualQualifiers() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + DerivedColumn rowNum = DerivedColumn.of("rownum()"); + + SelectStatementProvider selectStatement = select(animalName.qualifiedWith("b"), rowNum) + .from( + select(id, animalName) + .from(animalData, "a") + .where(id, isLessThan(22)) + .orderBy(animalName.descending()), + "b" + ) + .where(rowNum, isLessThan(5)) + .and(animalName.qualifiedWith("b"), isLike("%a%")) + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()).isEqualTo( + "select b.animal_name, rownum() " + + "from (select a.id, a.animal_name " + + "from AnimalData a where a.id < #{parameters.p1,jdbcType=INTEGER} " + + "order by animal_name DESC) b " + + "where rownum() < #{parameters.p2} and b.animal_name like #{parameters.p3,jdbcType=VARCHAR}" + ); + assertThat(selectStatement.getParameters()).containsEntry("p1", 22); + assertThat(selectStatement.getParameters()).containsEntry("p2", 5); + assertThat(selectStatement.getParameters()).containsEntry("p3", "%a%"); + + List> rows = mapper.selectManyMappedRows(selectStatement); + assertThat(rows).hasSize(4); + + assertThat(rows.get(2)).containsEntry("ANIMAL_NAME", "Chinchilla"); + assertThat(rows.get(2)).containsEntry("ROWNUM", 3); + } + } + + @Test + void testBasicSubQueryWithAliases() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + DerivedColumn rowNum = DerivedColumn.of("rownum()"); + SqlColumn outerAnimalName = animalName.qualifiedWith("b"); + DerivedColumn animalId = DerivedColumn.of("animalId", "b"); + + SelectStatementProvider selectStatement = select(outerAnimalName.asCamelCase(), animalId, rowNum) + .from( + select(id.as("animalId"), animalName) + .from(animalData, "a") + .where(id, isLessThan(22)) + .orderBy(animalName.descending()), + "b" + ) + .where(rowNum, isLessThan(5)) + .and(outerAnimalName, isLike("%a%")) + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()).isEqualTo( + "select b.animal_name as \"animalName\", b.animalId, rownum() " + + "from (select a.id as animalId, a.animal_name " + + "from AnimalData a where a.id < #{parameters.p1,jdbcType=INTEGER} " + + "order by animal_name DESC) b " + + "where rownum() < #{parameters.p2} and b.animal_name like #{parameters.p3,jdbcType=VARCHAR}" + ); + assertThat(selectStatement.getParameters()).containsEntry("p1", 22); + assertThat(selectStatement.getParameters()).containsEntry("p2", 5); + assertThat(selectStatement.getParameters()).containsEntry("p3", "%a%"); + + List> rows = mapper.selectManyMappedRows(selectStatement); + assertThat(rows).hasSize(4); + + assertThat(rows.get(2)).containsEntry("animalName", "Chinchilla"); + assertThat(rows.get(2)).containsEntry("ANIMALID", 14); + assertThat(rows.get(2)).containsEntry("ROWNUM", 3); + } + } +} diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt index 4b5309a2f..7974473e0 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt @@ -59,6 +59,9 @@ fun PersonMapper.insert(record: PersonRecord) = fun PersonMapper.generalInsert(completer: GeneralInsertCompleter) = insertInto(this::generalInsert, Person, completer) +fun PersonMapper.insertSelect(completer: InsertSelectCompleter) = + insertSelect(this::insertSelect, Person, completer) + fun PersonMapper.insertMultiple(vararg records: PersonRecord) = insertMultiple(records.toList()) diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt index 430eb2906..d857ae1e7 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt @@ -15,6 +15,7 @@ */ package examples.kotlin.mybatis3.canonical +import examples.kotlin.mybatis3.canonical.PersonDynamicSqlSupport.Person import examples.kotlin.mybatis3.canonical.PersonDynamicSqlSupport.Person.addressId import examples.kotlin.mybatis3.canonical.PersonDynamicSqlSupport.Person.birthDate import examples.kotlin.mybatis3.canonical.PersonDynamicSqlSupport.Person.employed @@ -218,6 +219,23 @@ class PersonMapperTest { } } + @Test + fun testInsertSelect() { + newSession().use { session -> + val mapper = session.getMapper(PersonMapper::class.java) + + val rows = mapper.insertSelect { + columns(id, firstName, lastName, employed, occupation, addressId, birthDate) + select(add(id, constant("100")), firstName, lastName, employed, occupation, addressId, birthDate) { + from(Person) + orderBy(id) + } + } + + assertThat(rows).isEqualTo(6) + } + } + @Test fun testInsertMultiple() { newSession().use { session -> diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapperExtensions.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapperExtensions.kt index 5f8337682..afc007f1d 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapperExtensions.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonWithAddressMapperExtensions.kt @@ -24,44 +24,40 @@ import examples.kotlin.mybatis3.canonical.PersonDynamicSqlSupport.Person.id import examples.kotlin.mybatis3.canonical.PersonDynamicSqlSupport.Person.lastName import examples.kotlin.mybatis3.canonical.PersonDynamicSqlSupport.Person.occupation import org.mybatis.dynamic.sql.SqlBuilder.* -import org.mybatis.dynamic.sql.util.kotlin.select -import org.mybatis.dynamic.sql.util.kotlin.selectDistinct +import org.mybatis.dynamic.sql.util.kotlin.KotlinSelectBuilder import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter import org.mybatis.dynamic.sql.util.kotlin.mybatis3.selectOne import org.mybatis.dynamic.sql.util.kotlin.mybatis3.selectList fun PersonWithAddressMapper.selectOne(completer: SelectCompleter): PersonWithAddress? { - val start = select(id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, Address.id, - Address.streetAddress, Address.city, Address.state) { - from(Person) - fullJoin(Address) { - on(Person.addressId, equalTo(Address.id)) - } - } + val start = KotlinSelectBuilder(select(id.`as`("A_ID"), firstName, lastName, birthDate, + employed, occupation, Address.id, Address.streetAddress, Address.city, Address.state)) + .from(Person) + .fullJoin(Address) { + on(Person.addressId, equalTo(Address.id)) + } return selectOne(this::selectOne, start, completer) } fun PersonWithAddressMapper.select(completer: SelectCompleter): List { - val start = select(id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, Address.id, - Address.streetAddress, Address.city, Address.state) { - from(Person, "p") - fullJoin(Address) { - on(Person.addressId, equalTo(Address.id)) - } - } + val start = KotlinSelectBuilder(select(id.`as`("A_ID"), firstName, lastName, birthDate, + employed, occupation, Address.id, Address.streetAddress, Address.city, Address.state)) + .from(Person, "p") + .fullJoin(Address) { + on(Person.addressId, equalTo(Address.id)) + } return selectList(this::selectMany, start, completer) } fun PersonWithAddressMapper.selectDistinct(completer: SelectCompleter): List { - val start = selectDistinct(id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, Address.id, - Address.streetAddress, Address.city, Address.state) { - from(Person, "p") - fullJoin(Address) { - on(Person.addressId, equalTo(Address.id)) - } - } + val start = KotlinSelectBuilder(selectDistinct(id.`as`("A_ID"), firstName, lastName, + birthDate, employed, occupation, Address.id, Address.streetAddress, Address.city, Address.state)) + .from(Person, "p") + .fullJoin(Address) { + on(Person.addressId, equalTo(Address.id)) + } return selectList(this::selectMany, start, completer) } diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt index cabaa7f32..a86316cd2 100644 --- a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt +++ b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt @@ -231,6 +231,36 @@ class CanonicalSpringKotlinTemplateDirectTest { assertThat(rows[1]).isEqualTo(1) } + @Test + fun testInsertSelect() { + val rows = template.insertSelect(Person) { + columns(id, firstName, lastName, birthDate, employed, occupation, addressId) + select(add(id, constant("100")), firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + orderBy(id) + } + } + + assertThat(rows).isEqualTo(6) + + val records = template.select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isGreaterThanOrEqualTo(100)) + orderBy(id) + }.withRowMapper(personRowMapper) + + assertThat(records).hasSize(6) + with(records[1]) { + assertThat(id).isEqualTo(102) + assertThat(firstName).isEqualTo("Wilma") + assertThat(lastName).isEqualTo(LastName("Flintstone")) + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Accountant") + assertThat(addressId).isEqualTo(1) + } + } + @Test fun testGeneralInsertWithGeneratedKey() { val keyHolder = GeneratedKeyHolder() diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt index a1a1da04f..fea2cf840 100644 --- a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt +++ b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt @@ -339,6 +339,43 @@ class CanonicalSpringKotlinTest { assertThat(rows[1]).isEqualTo(1) } + @Test + fun testInsertSelect() { + val insertStatement = insertSelect(Person) { + columns(id, firstName, lastName, birthDate, employed, occupation, addressId) + select(add(id, constant("100")), firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + orderBy(id) + } + } + + assertThat(insertStatement.insertStatement).isEqualTo( + "insert into Person (id, first_name, last_name, birth_date, employed, occupation, address_id) " + + "select (id + 100), first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person " + + "order by id" + ) + val rows = template.insertSelect(insertStatement) + assertThat(rows).isEqualTo(6) + + val records = template.select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isGreaterThanOrEqualTo(100)) + orderBy(id) + }.withRowMapper(personRowMapper) + + assertThat(records).hasSize(6) + with(records[1]) { + assertThat(id).isEqualTo(102) + assertThat(firstName).isEqualTo("Wilma") + assertThat(lastName).isEqualTo(LastName("Flintstone")) + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Accountant") + assertThat(addressId).isEqualTo(1) + } + } + @Test fun testGeneralInsertWithGeneratedKey() { val insertStatement = insertInto(GeneratedAlways) { diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/KotlinConditionsTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/KotlinConditionsTest.kt new file mode 100644 index 000000000..07ebe56a6 --- /dev/null +++ b/src/test/kotlin/examples/kotlin/spring/canonical/KotlinConditionsTest.kt @@ -0,0 +1,320 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.kotlin.spring.canonical + +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.addressId +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.birthDate +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.employed +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.firstName +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.id +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.lastName +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.occupation +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mybatis.dynamic.sql.SqlBuilder.isEqualTo +import org.mybatis.dynamic.sql.SqlBuilder.max +import org.mybatis.dynamic.sql.SqlBuilder.min +import org.mybatis.dynamic.sql.util.kotlin.isEqualTo +import org.mybatis.dynamic.sql.util.kotlin.isGreaterThan +import org.mybatis.dynamic.sql.util.kotlin.isGreaterThanOrEqualTo +import org.mybatis.dynamic.sql.util.kotlin.isIn +import org.mybatis.dynamic.sql.util.kotlin.isLessThan +import org.mybatis.dynamic.sql.util.kotlin.isLessThanOrEqualTo +import org.mybatis.dynamic.sql.util.kotlin.isNotEqualTo +import org.mybatis.dynamic.sql.util.kotlin.isNotIn +import org.mybatis.dynamic.sql.util.kotlin.spring.select +import org.mybatis.dynamic.sql.util.kotlin.spring.selectList +import org.mybatis.dynamic.sql.util.kotlin.spring.selectOne +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType + +@Suppress("LargeClass", "MaxLineLength") +class KotlinConditionsTest { + private lateinit var template: NamedParameterJdbcTemplate + + @BeforeEach + fun setup() { + val db = with(EmbeddedDatabaseBuilder()) { + setType(EmbeddedDatabaseType.HSQL) + generateUniqueName(true) + addScript("classpath:/examples/kotlin/spring/CreateGeneratedAlwaysDB.sql") + addScript("classpath:/examples/kotlin/spring/CreateSimpleDB.sql") + build() + } + template = NamedParameterJdbcTemplate(db) + } + + @Test + fun testSelectEqualSubQuery() { + val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isEqualTo { + select(max(id)) { + from(Person) + } + }) + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select id, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person where id = (select max(id) from Person)" + ) + + val row = template.selectOne(selectStatement, personRowMapper) + + assertThat(row).isNotNull() + with(row!!) { + assertThat(id).isEqualTo(6) + assertThat(firstName).isEqualTo("Bamm Bamm") + assertThat(lastName!!.name).isEqualTo("Rubble") + assertThat(birthDate).isNotNull() + assertThat(employed).isFalse() + assertThat(occupation).isNull() + assertThat(addressId).isEqualTo(2) + } + } + + @Test + fun testSelectNotEqualSubQuery() { + val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isNotEqualTo { + select(max(id)) { + from(Person) + } + }) + orderBy(id) + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select id, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person where id <> (select max(id) from Person) " + + "order by id" + ) + + val rows = template.selectList(selectStatement, personRowMapper) + + assertThat(rows).hasSize(5) + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName!!.name).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + } + + @Test + fun testInSubQuery() { + val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isIn { + select(id) { + from(Person) + where(lastName, isEqualTo(LastName("Rubble"))) + } + }) + orderBy(id) + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select id, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person where id in (select id from Person where last_name = :p1) " + + "order by id" + ) + assertThat(selectStatement.parameters).containsEntry("p1", "Rubble") + + val rows = template.selectList(selectStatement, personRowMapper) + + assertThat(rows).hasSize(3) + with(rows[0]) { + assertThat(id).isEqualTo(4) + assertThat(firstName).isEqualTo("Barney") + assertThat(lastName!!.name).isEqualTo("Rubble") + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(2) + } + } + + @Test + fun testNotInSubQuery() { + val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isNotIn { + selectDistinct(id) { + from(Person) + where(lastName, isEqualTo(LastName("Rubble"))) + } + }) + orderBy(id) + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select id, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person where id not in (select distinct id from Person where last_name = :p1) " + + "order by id" + ) + assertThat(selectStatement.parameters).containsEntry("p1", "Rubble") + + val rows = template.selectList(selectStatement, personRowMapper) + + assertThat(rows).hasSize(3) + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName!!.name).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + } + + @Test + fun testLessThanSubQuery() { + val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isLessThan { + select(max(id)) { + from(Person) + } + }) + orderBy(id) + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select id, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person where id < (select max(id) from Person) " + + "order by id" + ) + + val rows = template.selectList(selectStatement, personRowMapper) + + assertThat(rows).hasSize(5) + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName!!.name).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + } + + @Test + fun testLessThanOrEqualSubQuery() { + val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isLessThanOrEqualTo { + select(max(id)) { + from(Person) + } + }) + orderBy(id) + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select id, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person where id <= (select max(id) from Person) " + + "order by id" + ) + + val rows = template.selectList(selectStatement, personRowMapper) + + assertThat(rows).hasSize(6) + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName!!.name).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + } + + @Test + fun testGreaterThanSubQuery() { + val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isGreaterThan { + select(min(id)) { + from(Person) + } + }) + orderBy(id) + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select id, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person where id > (select min(id) from Person) " + + "order by id" + ) + + val rows = template.selectList(selectStatement, personRowMapper) + + assertThat(rows).hasSize(5) + with(rows[0]) { + assertThat(id).isEqualTo(2) + assertThat(firstName).isEqualTo("Wilma") + assertThat(lastName!!.name).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Accountant") + assertThat(addressId).isEqualTo(1) + } + } + + @Test + fun testGreaterThanOrEqualSubQuery() { + val selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) { + from(Person) + where(id, isGreaterThanOrEqualTo { + select(min(id)) { + from(Person) + } + }) + orderBy(id) + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select id, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person where id >= (select min(id) from Person) " + + "order by id" + ) + + val rows = template.selectList(selectStatement, personRowMapper) + + assertThat(rows).hasSize(6) + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName!!.name).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + } +} diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinSubQueryTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinSubQueryTest.kt new file mode 100644 index 000000000..6cb21e5df --- /dev/null +++ b/src/test/kotlin/examples/kotlin/spring/canonical/SpringKotlinSubQueryTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.kotlin.spring.canonical + +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.firstName +import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.id +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mybatis.dynamic.sql.DerivedColumn +import org.mybatis.dynamic.sql.SqlBuilder.* +import org.mybatis.dynamic.sql.util.kotlin.spring.selectList +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType +import org.mybatis.dynamic.sql.util.kotlin.spring.select + +class SpringKotlinSubQueryTest { + private lateinit var template: NamedParameterJdbcTemplate + + @BeforeEach + fun setup() { + val db = with(EmbeddedDatabaseBuilder()) { + setType(EmbeddedDatabaseType.HSQL) + generateUniqueName(true) + addScript("classpath:/examples/kotlin/spring/CreateSimpleDB.sql") + build() + } + template = NamedParameterJdbcTemplate(db) + } + + @Test + fun testBasicSubQuery() { + val rowNum = DerivedColumn.of("rownum()") + + val selectStatement = + select(firstName, rowNum) { + from { + select(id, firstName) { + from(Person) + where(id, isLessThan(22)) + orderBy(firstName.descending()) + } + } + where(rowNum, isLessThan(5)) + and(firstName, isLike("%a%")) + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select first_name, rownum() " + + "from (select id, first_name " + + "from Person where id < :p1 " + + "order by first_name DESC) " + + "where rownum() < :p2 and first_name like :p3" + ) + + assertThat(selectStatement.parameters).containsEntry("p1", 22) + assertThat(selectStatement.parameters).containsEntry("p2", 5) + assertThat(selectStatement.parameters).containsEntry("p3", "%a%") + + val rows = template.selectList(selectStatement) { rs, _ -> + mapOf( + Pair("FIRST_NAME", rs.getString(1)), + Pair("ROWNUM", rs.getInt(2)) + ) + } + + assertThat(rows).hasSize(3) + assertThat(rows[2]).containsEntry("FIRST_NAME", "Wilma") + assertThat(rows[2]).containsEntry("ROWNUM", 3) + } + + @Test + fun testBasicSubQueryTemplateDirect() { + val rowNum = DerivedColumn.of("rownum()") + + val rows = template.select(firstName, rowNum) { + from { + select(id, firstName) { + from(Person) + where(id, isLessThan(22)) + orderBy(firstName.descending()) + } + } + where(rowNum, isLessThan(5)) + and(firstName, isLike("%a%")) + }.withRowMapper { rs, _ -> + mapOf( + Pair("FIRST_NAME", rs.getString(1)), + Pair("ROWNUM", rs.getInt(2)) + ) + } + + assertThat(rows).hasSize(3) + assertThat(rows[2]).containsEntry("FIRST_NAME", "Wilma") + assertThat(rows[2]).containsEntry("ROWNUM", 3) + } + + @Test + fun testBasicSubQueryWithAliases() { + val rowNum = DerivedColumn.of("rownum()").`as`("myRows") + val outerFirstName = firstName.qualifiedWith("b") + val personId = DerivedColumn.of("personId", "b") + + val selectStatement = + select(outerFirstName.asCamelCase(), personId, rowNum) { + from { + select(id.`as`("personId"), firstName) { + from(Person, "a") + where(id, isLessThan(22)) + orderBy(firstName.descending()) + } + + "b" + } + where(rowNum, isLessThan(5)) + and(outerFirstName, isLike("%a%")) + } + + assertThat(selectStatement.selectStatement).isEqualTo( + "select b.first_name as \"firstName\", b.personId, rownum() as myRows " + + "from (select a.id as personId, a.first_name " + + "from Person a where a.id < :p1 " + + "order by first_name DESC) b " + + "where rownum() < :p2 and b.first_name like :p3" + ) + + assertThat(selectStatement.parameters).containsEntry("p1", 22) + assertThat(selectStatement.parameters).containsEntry("p2", 5) + assertThat(selectStatement.parameters).containsEntry("p3", "%a%") + + val rows = template.selectList(selectStatement) { rs, _ -> + mapOf( + Pair("firstName", rs.getString("firstName")), + Pair("PERSONID", rs.getInt("PERSONID")), + Pair("ROWNUM", rs.getInt("MYROWS")) + ) + } + + assertThat(rows).hasSize(3) + assertThat(rows[2]).containsEntry("firstName", "Wilma") + assertThat(rows[2]).containsEntry("PERSONID", 2) + assertThat(rows[2]).containsEntry("ROWNUM", 3) + } +}