diff --git a/CHANGELOG.md b/CHANGELOG.md index 13fab73d8..513bc8d72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ If you have written your own set of functions to extend the library, you will no - Added the ability to write a function that will change the column data type ([#197](https://github.com/mybatis/mybatis-dynamic-sql/issues/197)) - Added the `applyOperator` function to make it easy to use non-standard database operators in expressions ([#220](https://github.com/mybatis/mybatis-dynamic-sql/issues/220)) - Added convenience methods for count(column) and count(distinct column)([#221](https://github.com/mybatis/mybatis-dynamic-sql/issues/221)) +- Added support for union queries in Kotlin([#187](https://github.com/mybatis/mybatis-dynamic-sql/issues/187)) ## Release 1.1.4 - November 23, 2019 diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/CriteriaCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/CriteriaCollector.kt index 14b342824..6f4e4c16a 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/CriteriaCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/CriteriaCollector.kt @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * 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. @@ -21,6 +21,7 @@ import org.mybatis.dynamic.sql.VisitableCondition typealias CriteriaReceiver = CriteriaCollector.() -> CriteriaCollector +@MyBatisDslMarker class CriteriaCollector { val criteria = mutableListOf>() diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt index 78f0aef5d..3524f417f 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * 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. @@ -21,6 +21,7 @@ import org.mybatis.dynamic.sql.select.join.JoinCriterion typealias JoinReceiver = JoinCollector.() -> JoinCollector +@MyBatisDslMarker class JoinCollector { val onJoinCriterion: JoinCriterion by lazy { internalOnCriterion } val andJoinCriteria = mutableListOf() diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt index 77d3135c4..c4c1785fd 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * 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. @@ -20,10 +20,12 @@ import org.mybatis.dynamic.sql.SqlTable import org.mybatis.dynamic.sql.VisitableCondition import org.mybatis.dynamic.sql.select.AbstractQueryExpressionDSL import org.mybatis.dynamic.sql.select.SelectModel -import org.mybatis.dynamic.sql.util.Buildable import org.mybatis.dynamic.sql.where.AbstractWhereDSL -abstract class KotlinBaseBuilder, B : KotlinBaseBuilder> : Buildable { +@DslMarker annotation class MyBatisDslMarker + +@MyBatisDslMarker +abstract class KotlinBaseBuilder, B : KotlinBaseBuilder> { fun where(column: BindableColumn, condition: VisitableCondition): B = applySelf { getWhere().where(column, condition) @@ -69,7 +71,7 @@ abstract class KotlinBaseBuilder, B : KotlinBaseBuild abstract class KotlinBaseJoiningBuilder, W : AbstractWhereDSL, B : KotlinBaseJoiningBuilder>( private val dsl: AbstractQueryExpressionDSL -) : KotlinBaseBuilder() { +) : KotlinBaseBuilder() { fun join(table: SqlTable, receiver: JoinReceiver): B = applySelf { diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinCountBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinCountBuilder.kt index eed0caeaa..5164c4ad5 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinCountBuilder.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinCountBuilder.kt @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * 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. @@ -22,7 +22,7 @@ import org.mybatis.dynamic.sql.util.Buildable typealias CountCompleter = KotlinCountBuilder.() -> Buildable class KotlinCountBuilder(private val dsl: CountDSL) : - KotlinBaseJoiningBuilder, CountDSL.CountWhereBuilder, KotlinCountBuilder>(dsl) { + KotlinBaseJoiningBuilder, CountDSL.CountWhereBuilder, KotlinCountBuilder>(dsl), Buildable { fun allRows() = this diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinDeleteBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinDeleteBuilder.kt index a986596bb..61053e0f8 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinDeleteBuilder.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinDeleteBuilder.kt @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * 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. @@ -22,7 +22,7 @@ import org.mybatis.dynamic.sql.util.Buildable typealias DeleteCompleter = KotlinDeleteBuilder.() -> Buildable class KotlinDeleteBuilder(private val dsl: DeleteDSL) : - KotlinBaseBuilder.DeleteWhereBuilder, KotlinDeleteBuilder>() { + KotlinBaseBuilder.DeleteWhereBuilder, KotlinDeleteBuilder>(), Buildable { fun allRows() = this diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinQueryBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinQueryBuilder.kt index 60976a20d..2a2ad07b0 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinQueryBuilder.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinQueryBuilder.kt @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * 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. @@ -24,7 +24,7 @@ import org.mybatis.dynamic.sql.util.Buildable typealias SelectCompleter = KotlinQueryBuilder.() -> Buildable class KotlinQueryBuilder(private val dsl: QueryExpressionDSL) : - KotlinBaseJoiningBuilder, QueryExpressionDSL.QueryExpressionWhereBuilder, KotlinQueryBuilder>(dsl) { + KotlinBaseJoiningBuilder, QueryExpressionDSL.QueryExpressionWhereBuilder, KotlinQueryBuilder>(dsl), Buildable { fun groupBy(vararg columns: BasicColumn) = apply { @@ -50,6 +50,16 @@ class KotlinQueryBuilder(private val dsl: QueryExpressionDSL) : fun allRows() = this + fun union(union: KotlinUnionBuilder.() -> Unit) = + apply { + union(KotlinUnionBuilder(dsl.union())) + } + + fun unionAll(unionAll: KotlinUnionBuilder.() -> Unit) = + apply { + unionAll(KotlinUnionBuilder(dsl.unionAll())) + } + override fun build(): SelectModel = dsl.build() override fun getWhere(): QueryExpressionDSL.QueryExpressionWhereBuilder = dsl.where() diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUnionBuilders.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUnionBuilders.kt new file mode 100644 index 000000000..b9d541b75 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUnionBuilders.kt @@ -0,0 +1,54 @@ +/** + * 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.SqlTable +import org.mybatis.dynamic.sql.select.QueryExpressionDSL +import org.mybatis.dynamic.sql.select.SelectModel + +@MyBatisDslMarker +class KotlinUnionBuilder(private val unionBuilder: QueryExpressionDSL.UnionBuilder) { + fun select(vararg selectList: BasicColumn) = + select(selectList.toList()) + + fun select(selectList: List) = + KotlinUnionFromGatherer(unionBuilder.select(selectList)) + + fun selectDistinct(vararg selectList: BasicColumn) = + selectDistinct(selectList.toList()) + + fun selectDistinct(selectList: List) = + KotlinUnionFromGatherer(unionBuilder.selectDistinct(selectList)) +} + +class KotlinUnionFromGatherer(private val fromGatherer: QueryExpressionDSL.FromGatherer) { + fun from(table: SqlTable, enhance: KotlinUnionQueryBuilder.() -> Unit) = + enhance(KotlinUnionQueryBuilder(fromGatherer.from(table))) + + fun from(table: SqlTable, alias: String, enhance: KotlinUnionQueryBuilder.() -> Unit) = + enhance(KotlinUnionQueryBuilder(fromGatherer.from(table, alias))) +} + +class KotlinUnionQueryBuilder(private val unionDsl: QueryExpressionDSL) : + KotlinBaseJoiningBuilder, QueryExpressionDSL.QueryExpressionWhereBuilder, + KotlinUnionQueryBuilder>(unionDsl) { + fun allRows() = this + + override fun self() = this + + override fun getWhere(): QueryExpressionDSL.QueryExpressionWhereBuilder = unionDsl.where() +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt index 836e20cc3..78214158f 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt @@ -33,7 +33,7 @@ typealias MultiRowInsertCompleter = MultiRowInsertDSL.() -> MultiRowInsert typealias UpdateCompleter = KotlinUpdateBuilder.() -> Buildable class KotlinUpdateBuilder(private val dsl: UpdateDSL) : - KotlinBaseBuilder.UpdateWhereBuilder, KotlinUpdateBuilder>() { + KotlinBaseBuilder.UpdateWhereBuilder, KotlinUpdateBuilder>(), Buildable { fun set(column: SqlColumn): UpdateDSL.SetClauseFinisher = dsl.set(column) diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt index fb1eb9c03..5da44740d 100644 --- a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt +++ b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt @@ -360,6 +360,299 @@ class CanonicalSpringKotlinTest { } } + @Test + fun testRawSelectWithUnion() { + val selectStatement = select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person) { + where(id, isEqualTo(1)) + union { + select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person) { + where(id, isEqualTo(2)) + } + } + union { + select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person) { + where(id, isEqualTo(3)) + } + } + } + + val expected = "select id as A_ID, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person " + + "where id = :p1 " + + "union " + + "select id as A_ID, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person " + + "where id = :p2 " + + "union " + + "select id as A_ID, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person " + + "where id = :p3" + + assertThat(selectStatement.selectStatement).isEqualTo(expected) + + val records = template.selectList(selectStatement) { rs, _ -> + val record = PersonRecord() + record.id = rs.getInt(1) + record.firstName = rs.getString(2) + record.lastName = rs.getString(3) + record.birthDate = rs.getTimestamp(4) + record.employed = rs.getString(5) + record.occupation = rs.getString(6) + record.addressId = rs.getInt(7) + record + } + + assertThat(records).hasSize(3) + with(records[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isEqualTo("Yes") + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + + with(records[2]) { + assertThat(id).isEqualTo(3) + assertThat(firstName).isEqualTo("Pebbles") + assertThat(lastName).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isEqualTo("No") + assertThat(occupation).isNull() + assertThat(addressId).isEqualTo(1) + } + } + + @Test + fun testRawSelectWithUnionAndAlias() { + val selectStatement = select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person) { + where(id, isEqualTo(1)) + union { + select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person) { + where(id, isEqualTo(2)) + } + } + union { + select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person, "p") { + where(id, isEqualTo(3)) + } + } + } + + val expected = "select id as A_ID, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person " + + "where id = :p1 " + + "union " + + "select id as A_ID, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person " + + "where id = :p2 " + + "union " + + "select p.id as A_ID, p.first_name, p.last_name, p.birth_date, p.employed, p.occupation, p.address_id " + + "from Person p " + + "where p.id = :p3" + + assertThat(selectStatement.selectStatement).isEqualTo(expected) + + val records = template.selectList(selectStatement) { rs, _ -> + val record = PersonRecord() + record.id = rs.getInt(1) + record.firstName = rs.getString(2) + record.lastName = rs.getString(3) + record.birthDate = rs.getTimestamp(4) + record.employed = rs.getString(5) + record.occupation = rs.getString(6) + record.addressId = rs.getInt(7) + record + } + + assertThat(records).hasSize(3) + with(records[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isEqualTo("Yes") + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + + with(records[2]) { + assertThat(id).isEqualTo(3) + assertThat(firstName).isEqualTo("Pebbles") + assertThat(lastName).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isEqualTo("No") + assertThat(occupation).isNull() + assertThat(addressId).isEqualTo(1) + } + } + + @Test + fun testRawSelectWithUnionAndDistinct() { + val selectStatement = select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person) { + where(id, isEqualTo(1)) + union { + select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person) { + where(id, isEqualTo(2)) + } + } + union { + selectDistinct( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person, "p") { + where(id, isEqualTo(3)) + } + } + } + + val expected = "select id as A_ID, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person " + + "where id = :p1 " + + "union " + + "select id as A_ID, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person " + + "where id = :p2 " + + "union " + + "select distinct p.id as A_ID, p.first_name, p.last_name, p.birth_date, p.employed, p.occupation, p.address_id " + + "from Person p " + + "where p.id = :p3" + + assertThat(selectStatement.selectStatement).isEqualTo(expected) + + val records = template.selectList(selectStatement) { rs, _ -> + val record = PersonRecord() + record.id = rs.getInt(1) + record.firstName = rs.getString(2) + record.lastName = rs.getString(3) + record.birthDate = rs.getTimestamp(4) + record.employed = rs.getString(5) + record.occupation = rs.getString(6) + record.addressId = rs.getInt(7) + record + } + + assertThat(records).hasSize(3) + with(records[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isEqualTo("Yes") + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + + with(records[2]) { + assertThat(id).isEqualTo(3) + assertThat(firstName).isEqualTo("Pebbles") + assertThat(lastName).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isEqualTo("No") + assertThat(occupation).isNull() + assertThat(addressId).isEqualTo(1) + } + } + + @Test + fun testRawSelectWithUnionAllAndDistinct() { + val selectStatement = select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person) { + where(id, isEqualTo(1)) + union { + select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person) { + where(id, isEqualTo(2)) + } + } + unionAll { + selectDistinct( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person, "p") { + allRows() + } + } + orderBy(sortColumn("A_ID")) + } + + val expected = "select id as A_ID, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person " + + "where id = :p1 " + + "union " + + "select id as A_ID, first_name, last_name, birth_date, employed, occupation, address_id " + + "from Person " + + "where id = :p2 " + + "union all " + + "select distinct p.id as A_ID, p.first_name, p.last_name, p.birth_date, p.employed, p.occupation, p.address_id " + + "from Person p " + + "order by A_ID" + + assertThat(selectStatement.selectStatement).isEqualTo(expected) + + val records = template.selectList(selectStatement) { rs, _ -> + val record = PersonRecord() + record.id = rs.getInt(1) + record.firstName = rs.getString(2) + record.lastName = rs.getString(3) + record.birthDate = rs.getTimestamp(4) + record.employed = rs.getString(5) + record.occupation = rs.getString(6) + record.addressId = rs.getInt(7) + record + } + + assertThat(records).hasSize(8) + with(records[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isEqualTo("Yes") + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + + with(records[2]) { + assertThat(id).isEqualTo(2) + assertThat(firstName).isEqualTo("Wilma") + assertThat(lastName).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isEqualTo("Yes") + assertThat(occupation).isEqualTo("Accountant") + assertThat(addressId).isEqualTo(1) + } + } + @Test fun testRawSelectWithJoin() { val selectStatement = select(