From 5d88ce1d6cc085864adcbd3f0352d8db7a173d8d Mon Sep 17 00:00:00 2001 From: AndreasVolkmann Date: Mon, 18 Feb 2019 18:51:08 +0100 Subject: [PATCH] rework joins --- build.gradle | 2 + core/out/test/resources/logback.xml | 35 ++++ .../andrewoma/kwery/core/builder/Join.kt | 27 +++ .../andrewoma/kwery/core/builder/JoinGroup.kt | 39 ++++ .../andrewoma/kwery/core/builder/Query.kt | 12 +- .../kwery/core/builder/QueryBuilder.kt | 30 +++- .../kwery/core/builder/AdvancedTests.kt | 166 ++++++++++++++++++ .../andrewoma/kwery/core/builder/TestUtil.kt | 6 + 8 files changed, 310 insertions(+), 7 deletions(-) create mode 100644 core/out/test/resources/logback.xml create mode 100644 core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/Join.kt create mode 100644 core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/JoinGroup.kt create mode 100644 core/src/test/kotlin/com/github/andrewoma/kwery/core/builder/AdvancedTests.kt create mode 100644 core/src/test/kotlin/com/github/andrewoma/kwery/core/builder/TestUtil.kt diff --git a/build.gradle b/build.gradle index e99a089..76d13db 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,7 @@ subprojects { } mavenCentral() mavenLocal() + jcenter() } dependencies { compile( @@ -143,6 +144,7 @@ project(':core') { "org.postgresql:postgresql:$postgresVersion", "mysql:mysql-connector-java:$mysqlVersion", "org.xerial:sqlite-jdbc:3.8.11.2", + "org.amshove.kluent:kluent:1.47" ) } } diff --git a/core/out/test/resources/logback.xml b/core/out/test/resources/logback.xml new file mode 100644 index 0000000..ffe6a3e --- /dev/null +++ b/core/out/test/resources/logback.xml @@ -0,0 +1,35 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/Join.kt b/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/Join.kt new file mode 100644 index 0000000..c4b774d --- /dev/null +++ b/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/Join.kt @@ -0,0 +1,27 @@ +package com.github.andrewoma.kwery.core.builder + +data class Join( + val type: Type, + val table: String, + val handler: String = table.split("_").map { it.first().toLowerCase() }.joinToString(""), + val andClauses: MutableList<(String) -> String> = mutableListOf(), + val builder: Join.(String) -> String +) { + + enum class Type(val value: String) { + Inner("inner"), + Left("left"), + Right("right") + } + + infix fun and(clause: (handler: String) -> String) = apply { andClauses.add(clause) } + + val on = builder(this, handler) + + fun on(clause: String) = clause + + fun getSql(): String { + val and = andClauses.joinToString(" ") { "and ${it(handler)}" } + return "${type.value} join $table $handler on $on $and".trim() + } +} diff --git a/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/JoinGroup.kt b/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/JoinGroup.kt new file mode 100644 index 0000000..d92a1b0 --- /dev/null +++ b/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/JoinGroup.kt @@ -0,0 +1,39 @@ +package com.github.andrewoma.kwery.core.builder + +import com.github.andrewoma.kwery.core.builder.Join.Type.* + +class JoinGroup(internal val builders: MutableList Unit> = mutableListOf()) { + + private var counter = 0 + private val joins = mutableMapOf() + private val literalJoins = mutableMapOf() + + internal fun addBuilder(builder: JoinGroup.() -> Unit) = builders.add(builder) + + fun Join.add(): Join = if (joins.any { it.value.handler == handler }) { + copy(handler = this.handler + "1").add() + } else this.also { joins[counter++] = it } + + fun innerJoin(table: String, handler: String? = null, builder: Join.(String) -> String) = + join(Inner, table, handler, builder) + + fun leftJoin(table: String, handler: String? = null, builder: Join.(String) -> String) = + join(Left, table, handler, builder) + + fun rightJoin(table: String, handler: String? = null, builder: Join.(String) -> String) = + join(Right, table, handler, builder) + + fun join(sql: String) = literalJoins.put(counter++, sql) + + fun join(type: Join.Type, table: String, handler: String?, builder: Join.(String) -> String): Join { + val join = handler?.let { Join(type, table, it, builder = builder) } + ?: Join(type, table, builder = builder) + return join.add() + } + + fun build(): String { + builders.forEach { it(this) } + val joins = (literalJoins + joins.mapValues { it.value.getSql() }).toSortedMap() + return joins.values.joinToString("\n") + } +} diff --git a/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/Query.kt b/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/Query.kt index 764507b..eb78a06 100644 --- a/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/Query.kt +++ b/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/Query.kt @@ -22,6 +22,14 @@ package com.github.andrewoma.kwery.core.builder -class Query(val sqlBuffer: StringBuilder, val parameters: Map) { +class Query(private val sqlBuffer: StringBuilder, val parameters: Map) { val sql: String by lazy { sqlBuffer.toString() } -} \ No newline at end of file + + private fun union(kQuery: Query, unionType: String) = Query( + StringBuilder("$sql $unionType ${kQuery.sql}"), + parameters + kQuery.parameters + ) + + infix fun union(kQuery: Query) = union(kQuery, unionType = "UNION") + infix fun unionAll(kQuery: Query) = union(kQuery, unionType = "UNION ALL") +} diff --git a/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/QueryBuilder.kt b/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/QueryBuilder.kt index f95a031..f13e51c 100644 --- a/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/QueryBuilder.kt +++ b/core/src/main/kotlin/com/github/andrewoma/kwery/core/builder/QueryBuilder.kt @@ -41,13 +41,14 @@ class QueryBuilder { private var groupBy: String? = null private var having: String? = null private var orderBy: String? = null + private val joins: JoinGroup = JoinGroup() fun select(table: String) { selects.add(table) } fun parameter(name: String, value: Any?) { - params.put(name, value) + params[name] = value } fun groupBy(groupBy: String) { @@ -62,13 +63,27 @@ class QueryBuilder { this.orderBy = orderBy } - fun whereGroup(operator: String = "and", block: FilterBuilder.() -> Unit) { - require(filters.countLeaves() == 0) { "There must be only one root filters group" } - filters = Filter.Group(operator) - block(FilterBuilder(filters)) + fun whereGroup(operator: String = "and", block: FilterBuilder.() -> Unit): Unit = when (filters.countLeaves()) { + 0 -> { + filters = Filter.Group(operator) + block(FilterBuilder(filters)) + } + 2 -> { + val wrapper = Filter.Group("and") + wrapper.filters.add(this@QueryBuilder.filters) + block(FilterBuilder(wrapper)) + filters = wrapper + } + else -> { + val group = Filter.Group(operator) + block(FilterBuilder(group)) + filters.filters.add(group) + Unit + } } inner class FilterBuilder(private val group: Filter.Group) { + val parameter = this@QueryBuilder::parameter fun where(where: String) { group.filters.add(Filter.Where(where)) @@ -81,10 +96,15 @@ class QueryBuilder { } } + fun joinGroup(block: JoinGroup.() -> Unit) { + joins.addBuilder(block) + } + fun build(sb: StringBuilder = StringBuilder(), block: QueryBuilder.() -> Unit): Query { block(this) selects.joinTo(sb, "\n") + if (joins.builders.isNotEmpty()) { sb.append("\n").append(joins.build()) } if (filters.countLeaves() != 0) { sb.append("\nwhere ") appendConditions(sb, filters, true) diff --git a/core/src/test/kotlin/com/github/andrewoma/kwery/core/builder/AdvancedTests.kt b/core/src/test/kotlin/com/github/andrewoma/kwery/core/builder/AdvancedTests.kt new file mode 100644 index 0000000..50bb063 --- /dev/null +++ b/core/src/test/kotlin/com/github/andrewoma/kwery/core/builder/AdvancedTests.kt @@ -0,0 +1,166 @@ +package com.github.andrewoma.kwery.core.builder + +import org.amshove.kluent.`should be equal to` +import org.amshove.kluent.`should equal` +import org.junit.Test + +class AdvancedTests { + + @Test fun `Multiple root where groups`() { + query { + select("SELECT * FROM PERSON p") + whereGroup { + where("p.PERS_ID = '25'") + } + whereGroup { + where("p.SITE_ID = '100'") + } + whereGroup { + where("p.MERGE = '500'") + } + }.sql.trimQuery() `should be equal to` """ + SELECT * FROM PERSON p + where + (p.PERS_ID = '25' and p.SITE_ID = '100') and p.MERGE = '500' + """.trimQuery() + } + + @Test fun `Single root where group`() { + query { + select("SELECT * FROM PERSON p") + whereGroup { + where("p.PERS_ID = '25'") + } + }.sql.trimQuery() `should be equal to` """ + SELECT * FROM PERSON p + where p.PERS_ID = '25' + """.trimQuery() + } + + @Test fun `Two root where groups`() { + query { + select("SELECT * FROM PERSON p") + whereGroup { + where("p.PERS_ID = '25'") + } + whereGroup { + where("p.SITE_ID = '500'") + } + }.sql.trimQuery() `should be equal to` """ + SELECT * FROM PERSON p + where p.PERS_ID = '25' and p.SITE_ID = '500' + """.trimQuery() + } + + @Test fun `Nested and multi where groups`() { + query { + select("SELECT * FROM PERSON p") + whereGroup { + where("p.PERS_ID = '25'") + } + whereGroup { + whereGroup { + where("p.SITE_ID = '500'") + where("p.SITE_ID = '500'") + where("p.SITE_ID = '500'") + } + } + }.sql.trimQuery() `should be equal to` """ + SELECT * FROM PERSON p + where p.PERS_ID = '25' and + (p.SITE_ID = '500' and p.SITE_ID = '500' and p.SITE_ID = '500') + """.trimQuery() + } + + @Test fun `Multiple join groups`() { + query { + select("SELECT * FROM PERSON p") + joinGroup { + innerJoin("SITE_PERS", "sp") { "$it.PERS_ID = p.PERS_ID" } + } + joinGroup { + innerJoin("SITE_PERS2", "sp2") { "$it.PERS_ID = p.PERS_ID" } + } + }.sql.trimQuery() `should be equal to` """ + SELECT * FROM PERSON p + inner join SITE_PERS sp on sp.PERS_ID = p.PERS_ID + inner join SITE_PERS2 sp2 on sp2.PERS_ID = p.PERS_ID + """.trimQuery() + } + + @Test fun `Multiple join and where groups`() { + query { + select("SELECT * FROM PERSON p") + whereGroup { + where("p.PERS_ID = '25'") + } + whereGroup { + where("p.SITE_ID = '100'") + } + joinGroup { + innerJoin("SITE_PERS", "sp") { "$it.PERS_ID = p.PERS_ID" } + innerJoin("SITE_PERS", "sp2") { "$it.PERS_ID = p.PERS_ID" } + } + joinGroup { + leftJoin("SOMETHING") { "$it.APB_ID = sp.Q_ID" } + } + }.sql.trimQuery() `should be equal to` """ + SELECT * FROM PERSON p + inner join SITE_PERS sp on sp.PERS_ID = p.PERS_ID + inner join SITE_PERS sp2 on sp2.PERS_ID = p.PERS_ID + left join SOMETHING s on s.APB_ID = sp.Q_ID + where p.PERS_ID = '25' and p.SITE_ID = '100' + """.trimQuery() + } + + @Test fun `Creating an where extension`() { + fun QueryBuilder.FilterBuilder.where() { + where("a = :b") + parameter(":b", "abc") + } + query { + whereGroup { + where() + } + }.parameters `should equal` mapOf(":b" to "abc") + } + + + @Test fun `Simple union sample`() { + (query { + select("SELECT * FROM SITE") + } union query { + select("SELECT * FROM PERSON") + }).sql `should be equal to` "SELECT * FROM SITE UNION SELECT * FROM PERSON" + } + + @Test fun `Unions with parameters`() { + val result = query { + select("SELECT p.ID AS ID, p.NAME FROM dbo.PERSON p") + whereGroup { + where("p.ID = :id") + parameter("id", 25) + } + } unionAll query { + select("SELECT dp.PERS_ID as ID, dp.PERS_NAME as NAME FROM dbi.PERSON dp") + joinGroup { + leftJoin("SITE_PERS", "sp") { "$it.SITE_ID = dp.SITE_ID" } + } + whereGroup { + where("p.ID = :dpid") + parameter("dpid", 25) + } + } + + result.sql `should be equal to` """ + SELECT p.ID AS ID, p.NAME FROM dbo.PERSON p + where p.ID = :id UNION ALL SELECT dp.PERS_ID as ID, dp.PERS_NAME as NAME FROM dbi.PERSON dp + left join SITE_PERS sp on sp.SITE_ID = dp.SITE_ID + where p.ID = :dpid + """.trimIndent() + + result.parameters["id"] `should equal` 25 + result.parameters["dpid"] `should equal` 25 + result.parameters.size `should be equal to` 2 + } +} diff --git a/core/src/test/kotlin/com/github/andrewoma/kwery/core/builder/TestUtil.kt b/core/src/test/kotlin/com/github/andrewoma/kwery/core/builder/TestUtil.kt new file mode 100644 index 0000000..051c901 --- /dev/null +++ b/core/src/test/kotlin/com/github/andrewoma/kwery/core/builder/TestUtil.kt @@ -0,0 +1,6 @@ +package com.github.andrewoma.kwery.core.builder + +fun String.trimQuery() = this + .trimIndent() + .split("\n") + .fold("") { acc, r -> "$acc\n${r.trim()}" }