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()}" }