Skip to content

Support Sub-Queries in Select Lists #940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,10 @@ static CountDistinct countDistinct(BasicColumn column) {
return CountDistinct.of(column);
}

static SubQueryColumn subQuery(Buildable<SelectModel> subQuery) {
return SubQueryColumn.of(subQuery.build());
}

static <T> Max<T> max(BindableColumn<T> column) {
return Max.of(column);
}
Expand Down
60 changes: 60 additions & 0 deletions src/main/java/org/mybatis/dynamic/sql/SubQueryColumn.java
Original file line number Diff line number Diff line change
@@ -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<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ public static <T> SqlParameterSource[] createBatch(List<T> rows) {
return SqlParameterSourceUtils.createBatch(tt);
}

public record RowHolder<T> (T row) {}
public record RowHolder<T>(T row) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

infix fun <T : Any> SimpleCaseModel<T>.`as`(alias: String): SimpleCaseModel<T> = 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <T : Any> max(column: BindableColumn<T>): Max<T> = SqlBuilder.max(column)

fun <T : Any> min(column: BindableColumn<T>): Min<T> = SqlBuilder.min(column)
Expand Down
55 changes: 53 additions & 2 deletions src/test/java/examples/joins/JoinMapperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Map<String, Object>> 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<Map<String, Object>> 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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -44,6 +48,7 @@ class JoinMapperNewSyntaxTest {
sqlSessionFactory = TestUtils.buildSqlSessionFactory {
withInitializationScript("/examples/kotlin/mybatis3/joins/CreateJoinDB.sql")
withMapper(JoinMapper::class)
withMapper(CommonSelectMapper::class)
}
}

Expand Down Expand Up @@ -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))
}
}
}