Skip to content

Commit 865b471

Browse files
authored
Merge pull request #940 from jeffgbutler/sub-query
Support Sub-Queries in Select Lists
2 parents a48c22e + 7ba9761 commit 865b471

File tree

7 files changed

+161
-3
lines changed

7 files changed

+161
-3
lines changed

src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,10 @@ static CountDistinct countDistinct(BasicColumn column) {
499499
return CountDistinct.of(column);
500500
}
501501

502+
static SubQueryColumn subQuery(Buildable<SelectModel> subQuery) {
503+
return SubQueryColumn.of(subQuery.build());
504+
}
505+
502506
static <T> Max<T> max(BindableColumn<T> column) {
503507
return Max.of(column);
504508
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2016-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.mybatis.dynamic.sql;
17+
18+
import java.util.Objects;
19+
import java.util.Optional;
20+
21+
import org.jspecify.annotations.Nullable;
22+
import org.mybatis.dynamic.sql.render.RenderingContext;
23+
import org.mybatis.dynamic.sql.select.SelectModel;
24+
import org.mybatis.dynamic.sql.select.render.SubQueryRenderer;
25+
import org.mybatis.dynamic.sql.util.FragmentAndParameters;
26+
27+
public class SubQueryColumn implements BasicColumn {
28+
private final SelectModel selectModel;
29+
private @Nullable String alias;
30+
31+
private SubQueryColumn(SelectModel selectModel) {
32+
this.selectModel = Objects.requireNonNull(selectModel);
33+
}
34+
35+
@Override
36+
public Optional<String> alias() {
37+
return Optional.ofNullable(alias);
38+
}
39+
40+
@Override
41+
public SubQueryColumn as(String alias) {
42+
SubQueryColumn answer = new SubQueryColumn(selectModel);
43+
answer.alias = alias;
44+
return answer;
45+
}
46+
47+
@Override
48+
public FragmentAndParameters render(RenderingContext renderingContext) {
49+
return SubQueryRenderer.withSelectModel(selectModel)
50+
.withRenderingContext(renderingContext)
51+
.withPrefix("(") //$NON-NLS-1$
52+
.withSuffix(")") //$NON-NLS-1$
53+
.build()
54+
.render();
55+
}
56+
57+
public static SubQueryColumn of(SelectModel selectModel) {
58+
return new SubQueryColumn(selectModel);
59+
}
60+
}

src/main/java/org/mybatis/dynamic/sql/util/spring/BatchInsertUtility.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,5 @@ public static <T> SqlParameterSource[] createBatch(List<T> rows) {
3939
return SqlParameterSourceUtils.createBatch(tt);
4040
}
4141

42-
public record RowHolder<T> (T row) {}
42+
public record RowHolder<T>(T row) {}
4343
}

src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package org.mybatis.dynamic.sql.util.kotlin.elements
1717

1818
import org.mybatis.dynamic.sql.DerivedColumn
1919
import org.mybatis.dynamic.sql.SqlColumn
20+
import org.mybatis.dynamic.sql.SubQueryColumn
2021
import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel
2122
import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel
2223

@@ -28,6 +29,8 @@ infix fun SearchedCaseModel.`as`(alias: String): SearchedCaseModel = this.`as`(a
2829

2930
infix fun <T : Any> SimpleCaseModel<T>.`as`(alias: String): SimpleCaseModel<T> = this.`as`(alias)
3031

32+
infix fun SubQueryColumn.`as`(alias: String): SubQueryColumn = this.`as`(alias)
33+
3134
/**
3235
* Adds a qualifier to a column for use with table aliases (typically in joins or sub queries).
3336
* This is as close to natural SQL syntax as we can get in Kotlin. Natural SQL would look like

src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import org.mybatis.dynamic.sql.SortSpecification
2626
import org.mybatis.dynamic.sql.SqlBuilder
2727
import org.mybatis.dynamic.sql.SqlColumn
2828
import org.mybatis.dynamic.sql.StringConstant
29+
import org.mybatis.dynamic.sql.SubQueryColumn
2930
import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel
3031
import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel
3132
import org.mybatis.dynamic.sql.select.aggregate.Avg
@@ -141,6 +142,9 @@ fun count(column: BasicColumn): Count = SqlBuilder.count(column)
141142

142143
fun countDistinct(column: BasicColumn): CountDistinct = SqlBuilder.countDistinct(column)
143144

145+
fun subQuery(subQuery: KotlinSubQueryBuilder.() -> Unit): SubQueryColumn =
146+
SubQueryColumn.of(KotlinSubQueryBuilder().apply(subQuery).build())
147+
144148
fun <T : Any> max(column: BindableColumn<T>): Max<T> = SqlBuilder.max(column)
145149

146150
fun <T : Any> min(column: BindableColumn<T>): Min<T> = SqlBuilder.min(column)

src/test/java/examples/joins/JoinMapperTest.java

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
package examples.joins;
1717

1818
import static examples.joins.ItemMasterDynamicSQLSupport.itemMaster;
19-
import static examples.joins.OrderDetailDynamicSQLSupport.*;
19+
import static examples.joins.OrderDetailDynamicSQLSupport.orderDetail;
2020
import static examples.joins.OrderLineDynamicSQLSupport.orderLine;
2121
import static examples.joins.OrderMasterDynamicSQLSupport.orderDate;
2222
import static examples.joins.OrderMasterDynamicSQLSupport.orderMaster;
23-
import static examples.joins.UserDynamicSQLSupport.*;
23+
import static examples.joins.UserDynamicSQLSupport.user;
2424
import static org.assertj.core.api.Assertions.assertThat;
2525
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
26+
import static org.assertj.core.api.Assertions.entry;
2627
import static org.mybatis.dynamic.sql.SqlBuilder.*;
2728

2829
import java.io.InputStream;
@@ -1261,4 +1262,54 @@ void testJoinWithConstant() {
12611262
assertThat(row).containsEntry("ITEM_ID", 33);
12621263
}
12631264
}
1265+
1266+
@Test
1267+
void testJoinWithGroupBy() {
1268+
try (SqlSession session = sqlSessionFactory.openSession()) {
1269+
CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
1270+
1271+
SelectStatementProvider selectStatement = select(orderMaster.orderId, count().as("linecount"))
1272+
.from(orderMaster, "om")
1273+
.join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId))
1274+
.groupBy(orderMaster.orderId)
1275+
.orderBy(orderDetail.orderId)
1276+
.build()
1277+
.render(RenderingStrategies.MYBATIS3);
1278+
1279+
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";
1280+
assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement);
1281+
1282+
List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
1283+
1284+
assertThat(rows).hasSize(2);
1285+
assertThat(rows.get(0)).containsOnly(entry("ORDER_ID", 1), entry("LINECOUNT", 2L));
1286+
assertThat(rows.get(1)).containsOnly(entry("ORDER_ID", 2), entry("LINECOUNT", 1L));
1287+
}
1288+
}
1289+
1290+
@Test
1291+
void testSubQuery() {
1292+
try (SqlSession session = sqlSessionFactory.openSession()) {
1293+
CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class);
1294+
1295+
SelectStatementProvider selectStatement = select(orderMaster.orderId,
1296+
subQuery(select(count())
1297+
.from(orderDetail, "od")
1298+
.where(orderMaster.orderId, isEqualTo(orderDetail.orderId))
1299+
).as("linecount"))
1300+
.from(orderMaster, "om")
1301+
.orderBy(orderMaster.orderId)
1302+
.build()
1303+
.render(RenderingStrategies.MYBATIS3);
1304+
1305+
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";
1306+
assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement);
1307+
1308+
List<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);
1309+
1310+
assertThat(rows).hasSize(2);
1311+
assertThat(rows.get(0)).containsOnly(entry("ORDER_ID", 1), entry("LINECOUNT", 2L));
1312+
assertThat(rows.get(1)).containsOnly(entry("ORDER_ID", 2), entry("LINECOUNT", 1L));
1313+
}
1314+
}
12641315
}

src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ import org.junit.jupiter.api.Test
3030
import org.junit.jupiter.api.TestInstance
3131
import org.mybatis.dynamic.sql.util.Messages
3232
import org.mybatis.dynamic.sql.util.kotlin.KInvalidSQLException
33+
import org.mybatis.dynamic.sql.util.kotlin.elements.`as`
3334
import org.mybatis.dynamic.sql.util.kotlin.elements.constant
35+
import org.mybatis.dynamic.sql.util.kotlin.elements.count
3436
import org.mybatis.dynamic.sql.util.kotlin.elements.invoke
37+
import org.mybatis.dynamic.sql.util.kotlin.elements.subQuery
3538
import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select
39+
import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper
3640

3741
@Suppress("LargeClass")
3842
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -44,6 +48,7 @@ class JoinMapperNewSyntaxTest {
4448
sqlSessionFactory = TestUtils.buildSqlSessionFactory {
4549
withInitializationScript("/examples/kotlin/mybatis3/joins/CreateJoinDB.sql")
4650
withMapper(JoinMapper::class)
51+
withMapper(CommonSelectMapper::class)
4752
}
4853
}
4954

@@ -829,4 +834,35 @@ class JoinMapperNewSyntaxTest {
829834
}
830835
}.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$
831836
}
837+
838+
@Test
839+
fun testSubQuery() {
840+
sqlSessionFactory.openSession().use { session ->
841+
val mapper = session.getMapper(CommonSelectMapper::class.java)
842+
843+
val selectStatement = select(
844+
orderMaster.orderId, subQuery {
845+
select(count()) {
846+
from(orderDetail, "od")
847+
where {
848+
orderMaster.orderId isEqualTo orderDetail.orderId
849+
}
850+
}
851+
} `as` "linecount"
852+
) {
853+
from(orderMaster, "om")
854+
orderBy(orderMaster.orderId)
855+
}
856+
857+
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"
858+
859+
assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement)
860+
861+
val rows = mapper.selectManyMappedRows(selectStatement)
862+
863+
assertThat(rows).hasSize(2)
864+
assertThat(rows[0]).containsOnly(entry("ORDER_ID", 1), entry("LINECOUNT", 2L))
865+
assertThat(rows[1]).containsOnly(entry("ORDER_ID", 2), entry("LINECOUNT", 1L))
866+
}
867+
}
832868
}

0 commit comments

Comments
 (0)