From 8fb0de72747cbde5e43b19da72511c05ead1f74c Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 11 Apr 2025 17:44:13 -0400 Subject: [PATCH 1/3] Checkstyle --- .../org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) {} } From 3adcb2aedefa38262f295c54feeca4ac7758310f Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 11 Apr 2025 18:05:40 -0400 Subject: [PATCH 2/3] Add support for subqueries in a select list --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 4 ++ .../mybatis/dynamic/sql/SubQueryColumn.java | 60 +++++++++++++++++++ .../util/kotlin/elements/ColumnExtensions.kt | 3 + .../sql/util/kotlin/elements/SqlElements.kt | 4 ++ .../java/examples/joins/JoinMapperTest.java | 55 ++++++++++++++++- .../mybatis3/joins/JoinMapperNewSyntaxTest.kt | 36 +++++++++++ 6 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 9790633b0..84d50d556 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); + } + 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..abcd2f4b1 --- /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.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.Buildable; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class SubQueryColumn implements BasicColumn { + private final Buildable subQuery; + private @Nullable String alias; + + private SubQueryColumn(Buildable subQuery) { + this.subQuery = subQuery; + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public SubQueryColumn as(String alias) { + SubQueryColumn answer = new SubQueryColumn(subQuery); + answer.alias = alias; + return answer; + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + return SubQueryRenderer.withSelectModel(subQuery.build()) + .withRenderingContext(renderingContext) + .withPrefix("(") //$NON-NLS-1$ + .withSuffix(")") //$NON-NLS-1$ + .build() + .render(); + } + + public static SubQueryColumn of(Buildable subQuery) { + return new SubQueryColumn(subQuery); + } +} 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..3956beb0e 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)) + 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)) + } + } } From 7ba9761f62e1fe939127609871d6f1884e1d0e15 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Mon, 19 May 2025 10:38:50 -0400 Subject: [PATCH 3/3] Use select model instead of a builder --- .../java/org/mybatis/dynamic/sql/SqlBuilder.java | 2 +- .../org/mybatis/dynamic/sql/SubQueryColumn.java | 16 ++++++++-------- .../sql/util/kotlin/elements/SqlElements.kt | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 84d50d556..c8fe5c3ba 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -500,7 +500,7 @@ static CountDistinct countDistinct(BasicColumn column) { } static SubQueryColumn subQuery(Buildable subQuery) { - return SubQueryColumn.of(subQuery); + return SubQueryColumn.of(subQuery.build()); } static Max max(BindableColumn column) { diff --git a/src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java b/src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java index abcd2f4b1..cc35bdcad 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java @@ -15,21 +15,21 @@ */ 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.Buildable; import org.mybatis.dynamic.sql.util.FragmentAndParameters; public class SubQueryColumn implements BasicColumn { - private final Buildable subQuery; + private final SelectModel selectModel; private @Nullable String alias; - private SubQueryColumn(Buildable subQuery) { - this.subQuery = subQuery; + private SubQueryColumn(SelectModel selectModel) { + this.selectModel = Objects.requireNonNull(selectModel); } @Override @@ -39,14 +39,14 @@ public Optional alias() { @Override public SubQueryColumn as(String alias) { - SubQueryColumn answer = new SubQueryColumn(subQuery); + SubQueryColumn answer = new SubQueryColumn(selectModel); answer.alias = alias; return answer; } @Override public FragmentAndParameters render(RenderingContext renderingContext) { - return SubQueryRenderer.withSelectModel(subQuery.build()) + return SubQueryRenderer.withSelectModel(selectModel) .withRenderingContext(renderingContext) .withPrefix("(") //$NON-NLS-1$ .withSuffix(")") //$NON-NLS-1$ @@ -54,7 +54,7 @@ public FragmentAndParameters render(RenderingContext renderingContext) { .render(); } - public static SubQueryColumn of(Buildable subQuery) { - return new SubQueryColumn(subQuery); + public static SubQueryColumn of(SelectModel selectModel) { + return new SubQueryColumn(selectModel); } } 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 3956beb0e..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 @@ -143,7 +143,7 @@ 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)) + SubQueryColumn.of(KotlinSubQueryBuilder().apply(subQuery).build()) fun max(column: BindableColumn): Max = SqlBuilder.max(column)