diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 9790633b0..c8fe5c3ba 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -499,6 +499,10 @@ static CountDistinct countDistinct(BasicColumn column) { return CountDistinct.of(column); } + static SubQueryColumn subQuery(Buildable subQuery) { + return SubQueryColumn.of(subQuery.build()); + } + static Max max(BindableColumn column) { return Max.of(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java b/src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java new file mode 100644 index 000000000..cc35bdcad --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016-2025 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 + * + * https://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.util.Objects; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.render.SubQueryRenderer; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class SubQueryColumn implements BasicColumn { + private final SelectModel selectModel; + private @Nullable String alias; + + private SubQueryColumn(SelectModel selectModel) { + this.selectModel = Objects.requireNonNull(selectModel); + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public SubQueryColumn as(String alias) { + SubQueryColumn answer = new SubQueryColumn(selectModel); + answer.alias = alias; + return answer; + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + return SubQueryRenderer.withSelectModel(selectModel) + .withRenderingContext(renderingContext) + .withPrefix("(") //$NON-NLS-1$ + .withSuffix(")") //$NON-NLS-1$ + .build() + .render(); + } + + public static SubQueryColumn of(SelectModel selectModel) { + return new SubQueryColumn(selectModel); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java b/src/main/java/org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java index 8672e98ea..598a6b4a0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java @@ -39,5 +39,5 @@ public static SqlParameterSource[] createBatch(List rows) { return SqlParameterSourceUtils.createBatch(tt); } - public record RowHolder (T row) {} + public record RowHolder(T row) {} } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt index ab5f81a2a..c3651d995 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt @@ -17,6 +17,7 @@ package org.mybatis.dynamic.sql.util.kotlin.elements import org.mybatis.dynamic.sql.DerivedColumn import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.SubQueryColumn import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel @@ -28,6 +29,8 @@ infix fun SearchedCaseModel.`as`(alias: String): SearchedCaseModel = this.`as`(a infix fun SimpleCaseModel.`as`(alias: String): SimpleCaseModel = this.`as`(alias) +infix fun SubQueryColumn.`as`(alias: String): SubQueryColumn = this.`as`(alias) + /** * Adds a qualifier to a column for use with table aliases (typically in joins or sub queries). * This is as close to natural SQL syntax as we can get in Kotlin. Natural SQL would look like diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt index f648496d0..2002fc75a 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt @@ -26,6 +26,7 @@ import org.mybatis.dynamic.sql.SortSpecification import org.mybatis.dynamic.sql.SqlBuilder import org.mybatis.dynamic.sql.SqlColumn import org.mybatis.dynamic.sql.StringConstant +import org.mybatis.dynamic.sql.SubQueryColumn import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel import org.mybatis.dynamic.sql.select.aggregate.Avg @@ -141,6 +142,9 @@ fun count(column: BasicColumn): Count = SqlBuilder.count(column) fun countDistinct(column: BasicColumn): CountDistinct = SqlBuilder.countDistinct(column) +fun subQuery(subQuery: KotlinSubQueryBuilder.() -> Unit): SubQueryColumn = + SubQueryColumn.of(KotlinSubQueryBuilder().apply(subQuery).build()) + fun max(column: BindableColumn): Max = SqlBuilder.max(column) fun min(column: BindableColumn): Min = SqlBuilder.min(column) diff --git a/src/test/java/examples/joins/JoinMapperTest.java b/src/test/java/examples/joins/JoinMapperTest.java index 1f1c6a27c..2c9198aac 100644 --- a/src/test/java/examples/joins/JoinMapperTest.java +++ b/src/test/java/examples/joins/JoinMapperTest.java @@ -16,13 +16,14 @@ package examples.joins; import static examples.joins.ItemMasterDynamicSQLSupport.itemMaster; -import static examples.joins.OrderDetailDynamicSQLSupport.*; +import static examples.joins.OrderDetailDynamicSQLSupport.orderDetail; import static examples.joins.OrderLineDynamicSQLSupport.orderLine; import static examples.joins.OrderMasterDynamicSQLSupport.orderDate; import static examples.joins.OrderMasterDynamicSQLSupport.orderMaster; -import static examples.joins.UserDynamicSQLSupport.*; +import static examples.joins.UserDynamicSQLSupport.user; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; import static org.mybatis.dynamic.sql.SqlBuilder.*; import java.io.InputStream; @@ -1261,4 +1262,54 @@ void testJoinWithConstant() { assertThat(row).containsEntry("ITEM_ID", 33); } } + + @Test + void testJoinWithGroupBy() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, count().as("linecount")) + .from(orderMaster, "om") + .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + .groupBy(orderMaster.orderId) + .orderBy(orderDetail.orderId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, count(*) as linecount from OrderMaster om join OrderDetail od on om.order_id = od.order_id group by om.order_id order by order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + assertThat(rows.get(0)).containsOnly(entry("ORDER_ID", 1), entry("LINECOUNT", 2L)); + assertThat(rows.get(1)).containsOnly(entry("ORDER_ID", 2), entry("LINECOUNT", 1L)); + } + } + + @Test + void testSubQuery() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, + subQuery(select(count()) + .from(orderDetail, "od") + .where(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + ).as("linecount")) + .from(orderMaster, "om") + .orderBy(orderMaster.orderId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, (select count(*) from OrderDetail od where om.order_id = od.order_id) as linecount from OrderMaster om order by order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + assertThat(rows.get(0)).containsOnly(entry("ORDER_ID", 1), entry("LINECOUNT", 2L)); + assertThat(rows.get(1)).containsOnly(entry("ORDER_ID", 2), entry("LINECOUNT", 1L)); + } + } } diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt index 1ad6d71c2..3274d0595 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt @@ -30,9 +30,13 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.mybatis.dynamic.sql.util.Messages import org.mybatis.dynamic.sql.util.kotlin.KInvalidSQLException +import org.mybatis.dynamic.sql.util.kotlin.elements.`as` import org.mybatis.dynamic.sql.util.kotlin.elements.constant +import org.mybatis.dynamic.sql.util.kotlin.elements.count import org.mybatis.dynamic.sql.util.kotlin.elements.invoke +import org.mybatis.dynamic.sql.util.kotlin.elements.subQuery import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper @Suppress("LargeClass") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -44,6 +48,7 @@ class JoinMapperNewSyntaxTest { sqlSessionFactory = TestUtils.buildSqlSessionFactory { withInitializationScript("/examples/kotlin/mybatis3/joins/CreateJoinDB.sql") withMapper(JoinMapper::class) + withMapper(CommonSelectMapper::class) } } @@ -829,4 +834,35 @@ class JoinMapperNewSyntaxTest { } }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$ } + + @Test + fun testSubQuery() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + orderMaster.orderId, subQuery { + select(count()) { + from(orderDetail, "od") + where { + orderMaster.orderId isEqualTo orderDetail.orderId + } + } + } `as` "linecount" + ) { + from(orderMaster, "om") + orderBy(orderMaster.orderId) + } + + val expectedStatement = "select om.order_id, (select count(*) from OrderDetail od where om.order_id = od.order_id) as linecount from OrderMaster om order by order_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(2) + assertThat(rows[0]).containsOnly(entry("ORDER_ID", 1), entry("LINECOUNT", 2L)) + assertThat(rows[1]).containsOnly(entry("ORDER_ID", 2), entry("LINECOUNT", 1L)) + } + } }