diff --git a/README.md b/README.md
index 6e2d365..7b8b4aa 100644
--- a/README.md
+++ b/README.md
@@ -125,12 +125,23 @@ Query()
.order("user.id")
.limit(10, 20)
.toSql()
+
+// Join with subquery
+Query().fields("*")
+ .from("user")
+ .innerJoin(
+ Query().fields("id", "user_id", "(total_savings - total_spendings) as balance").from("report"),
+ "user_wallet"
+ )
+ .on("user_wallet.user_id = user.id")
+ .toSql()
```
Outputs
```sql
SELECT * FROM user LEFT JOIN address ON (address.user_id = user.id )
SELECT user.id as `user-id`, user.name as `user-name`, role.id as `role-id`, role.role_name as `role-role_name`, user.id as `role-user_id`, address.id as `address-id`, address.street1 as `address-street1`, address.street2 as `address-street2`, user.id as `address-user_id` FROM user LEFT JOIN address ON (address.user_id = user.id ) LEFT JOIN user_has_role ON (user_has_role.user_id = user.id ) LEFT JOIN role ON (role.id = user_has_role.role_id ) WHERE user.status > 0 OR user.id NOT IN (1,2,3) GROUP BY role.id, role.name HAVING SUM( role.id ) > 2 AND COUNT( role.id ) < 10 ORDER BY user.id ASC LIMIT 10 OFFSET 20
+SELECT * FROM user INNER JOIN ( SELECT id, user_id, (total_savings - total_spendings) as balance FROM report ) as user_wallet ON ( user_wallet.user_id = user.id )
```
#### MySQL Fulltext search
@@ -752,7 +763,7 @@ Add this to your maven pom.xml
io.zeko
zeko-sql-builder
- 1.3.1
+ 1.3.2
diff --git a/pom.xml b/pom.xml
index e418d6a..56c1bf4 100755
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
io.zeko
zeko-sql-builder
- 1.3.2-SNAPSHOT
+ 1.3.3-SNAPSHOT
jar
${project.groupId}:${project.artifactId}
diff --git a/src/main/kotlin/io/zeko/db/sql/Query.kt b/src/main/kotlin/io/zeko/db/sql/Query.kt
index 5370ed1..e42e25e 100644
--- a/src/main/kotlin/io/zeko/db/sql/Query.kt
+++ b/src/main/kotlin/io/zeko/db/sql/Query.kt
@@ -175,37 +175,82 @@ open class Query {
}
fun join(table: String): Query {
- tableToJoin["join-" + table] = arrayListOf()
+ tableToJoin["join-@" + table] = arrayListOf()
+ return this
+ }
+
+ fun join(table: Query, asName: String): Query {
+ tableToJoin["join-@**" + table.toSql() + "^^$asName"] = arrayListOf()
+ return this
+ }
+
+ fun fullJoin(table: String): Query {
+ tableToJoin["full-@join-@" + table] = arrayListOf()
+ return this
+ }
+
+ fun fullJoin(table: Query, asName: String): Query {
+ tableToJoin["full-@join-@**" + table.toSql() + "^^$asName"] = arrayListOf()
return this
}
fun leftJoin(table: String): Query {
- tableToJoin["left-join-" + table] = arrayListOf()
+ tableToJoin["left-@join-@" + table] = arrayListOf()
+ return this
+ }
+
+ fun leftJoin(table: Query, asName: String): Query {
+ tableToJoin["left-@join-@**" + table.toSql() + "^^$asName"] = arrayListOf()
return this
}
fun leftOuterJoin(table: String): Query {
- tableToJoin["left-outer-join-" + table] = arrayListOf()
+ tableToJoin["left-@outer-@join-@" + table] = arrayListOf()
+ return this
+ }
+
+ fun leftOuterJoin(table: Query, asName: String): Query {
+ tableToJoin["left-@outer-@join-@**" + table.toSql() + "^^$asName"] = arrayListOf()
return this
}
fun rightJoin(table: String): Query {
- tableToJoin["right-join-" + table] = arrayListOf()
+ tableToJoin["right-@join-@" + table] = arrayListOf()
+ return this
+ }
+
+ fun rightJoin(table: Query, asName: String): Query {
+ tableToJoin["right-@join-@**" + table.toSql() + "^^$asName"] = arrayListOf()
return this
}
fun rightOuterJoin(table: String): Query {
- tableToJoin["right-outer-join-" + table] = arrayListOf()
+ tableToJoin["right-@outer-@join-@" + table] = arrayListOf()
+ return this
+ }
+
+ fun rightOuterJoin(table: Query, asName: String): Query {
+ tableToJoin["right-@outer-@join-@**" + table.toSql() + "^^$asName"] = arrayListOf()
return this
}
fun innerJoin(table: String): Query {
- tableToJoin["inner-join-" + table] = arrayListOf()
+ tableToJoin["inner-@join-@" + table] = arrayListOf()
+ return this
+ }
+
+ fun innerJoin(table: Query, asName: String): Query {
+ tableToJoin["inner-@join-@**" + table.toSql() + "^^$asName"] = arrayListOf()
return this
}
fun crossJoin(table: String): Query {
- tableToJoin["cross-join-" + table] = arrayListOf()
+ tableToJoin["cross-@join-@" + table] = arrayListOf()
+ return this
+ }
+
+ fun crossJoin(table: Query, asName: String): Query {
+ tableToJoin["cross-@join-@**" + table.toSql() + "^^$asName"] = arrayListOf()
return this
}
diff --git a/src/main/kotlin/io/zeko/db/sql/QueryPart.kt b/src/main/kotlin/io/zeko/db/sql/QueryPart.kt
index d0cdc54..272218f 100644
--- a/src/main/kotlin/io/zeko/db/sql/QueryPart.kt
+++ b/src/main/kotlin/io/zeko/db/sql/QueryPart.kt
@@ -7,7 +7,6 @@ data class QueryInfo(val sql: String, val columns: List, val sqlFields:
class QueryParts {
private val rgxFindField = "([^\\\"\\ ][a-zA-Z0-9\\_]+[^\\\"\\ ])\\.([^\\\"\\s][a-zA-Z0-9\\_\\-\\=\\`\\~\\:\\.\\,\\|\\*\\^\\#\\@\\\$]+[\\^\"\\s])".toPattern()
- private val rgxReplace = "\\\"\$1\\\".\$2"
var linebreak: String = " "
get() = field
set(value) {
@@ -49,8 +48,9 @@ class QueryParts {
this.custom = customExpression
}
- private fun escapeTableName(statement: String): String {
+ private fun escapeTableName(statement: String, esp: String): String {
val matcher = rgxFindField.matcher(statement)
+ val rgxReplace = "\\" + esp + "\$1\\" + esp + ".\$2"
return matcher.replaceAll(rgxReplace)
}
@@ -82,7 +82,7 @@ class QueryParts {
val asTable = if (espTableName) "$esp$subTable$esp" else subTable
fromPart = "(${subParts.sql}) AS $asTable"
if (espTableName) {
- fromPart = escapeTableName(fromPart)
+ fromPart = escapeTableName(fromPart, esp)
}
} else {
fromPart = "(${subParts.sql})"
@@ -99,8 +99,23 @@ class QueryParts {
private fun buildJoinsPart(esp: String, espTableName: Boolean): String {
var joinsPart = ""
for ((join, conditions) in joins) {
- val parts = join.split("-")
- val tbl = if (espTableName) "$esp${parts.last()}$esp" else parts.last()
+ val parts = join.split("-@")
+ val lastPart = parts.last()
+ var asName = ""
+ val tbl = if (espTableName) {
+ if (lastPart.startsWith("**")) {
+ asName = lastPart.substring(lastPart.indexOf("^^") + 2)
+ "( ${lastPart.removePrefix("**").removeSuffix("^^$asName")} )"
+ } else
+ "$esp$lastPart$esp"
+ } else {
+ if (lastPart.startsWith("**")) {
+ asName = lastPart.substring(lastPart.indexOf("^^") + 2)
+ "( ${lastPart.removePrefix("**").removeSuffix("^^$asName")} )"
+ } else
+ lastPart
+ }
+
val joinStmt = parts.subList(0, parts.size - 1).joinToString(" ").toUpperCase()
var logicStmt = ""
@@ -110,12 +125,19 @@ class QueryParts {
if (parts.size > 0 && !parts[0].contains(".")) {
s = "${tbl}.${s.trimStart()}"
}
- logicStmt += if (espTableName) escapeTableName(s) else s
+ logicStmt += if (espTableName) escapeTableName(s, esp) else s
}
if (logicStmt != "") {
logicStmt = logicStmt.substring(0, logicStmt.length - 4)
- joinsPart += linebreak + "$joinStmt $tbl ON ($logicStmt)"
+ if (asName.isNotEmpty()) {
+ if (espTableName) {
+ asName = "$esp$asName$esp"
+ }
+ joinsPart += linebreak + "$joinStmt $tbl as $asName ON ($logicStmt)"
+ } else {
+ joinsPart += linebreak + "$joinStmt $tbl ON ($logicStmt)"
+ }
}
}
return joinsPart
@@ -125,7 +147,7 @@ class QueryParts {
var wherePart = ""
where.forEach {
val s = "${it.getStatement()} ${it.getOperator()} "
- wherePart += linebreak + (if (espTableName) escapeTableName(s) else s).trimEnd()
+ wherePart += linebreak + (if (espTableName) escapeTableName(s, esp) else s).trimEnd()
}
if (wherePart != "") {
@@ -139,7 +161,7 @@ class QueryParts {
if (groupBys.size > 0) {
groupBys.forEach {
if (espTableName) {
- groupByPart += escapeTableName("$it, ")
+ groupByPart += escapeTableName("$it, ", esp)
} else {
groupByPart += "$it, "
}
@@ -155,7 +177,7 @@ class QueryParts {
var havingPart = ""
havings.forEach {
val s = "${it.getStatement()} ${it.getOperator()} "
- havingPart += linebreak + (if (espTableName) escapeTableName(s) else s).trimEnd()
+ havingPart += linebreak + (if (espTableName) escapeTableName(s, esp) else s).trimEnd()
}
if (havingPart != "") {
@@ -168,7 +190,7 @@ class QueryParts {
var orderPart = ""
order.forEach {
val s = "${it.fieldName} ${it.getDirection()}, "
- orderPart += if (espTableName) escapeTableName(s) else s
+ orderPart += if (espTableName) escapeTableName(s, esp) else s
}
if (orderPart != "") {
diff --git a/src/test/kotlin/io/zeko/db/sql/ANSIJoinQuerySpec.kt b/src/test/kotlin/io/zeko/db/sql/ANSIJoinQuerySpec.kt
index a4bc06b..31df35f 100644
--- a/src/test/kotlin/io/zeko/db/sql/ANSIJoinQuerySpec.kt
+++ b/src/test/kotlin/io/zeko/db/sql/ANSIJoinQuerySpec.kt
@@ -140,6 +140,23 @@ class ANSIJoinQuerySpec : Spek({
LIMIT 10 OFFSET 20
""".trimIndent().replace("\n", ""), sql)
}
+
+ it("should match sql with one table inner join a subquery") {
+ val sql = ANSIQuery().fields("*")
+ .from("user")
+ .innerJoin(
+ ANSIQuery().fields("id", "user_id", "(total_savings - total_spendings) as balance").from("report"),
+ "user_wallet"
+ )
+ .on("user_wallet.user_id = user.id")
+ .toSql()
+ debug(sql)
+ assertEquals("""
+ SELECT * FROM "user" INNER JOIN (
+ SELECT id, user_id, (total_savings - total_spendings) as balance FROM "report" ) as "user_wallet"
+ ON ( "user_wallet".user_id = "user".id )
+ """.trimIndent().replace("\n", ""), sql)
+ }
}
}
})
diff --git a/src/test/kotlin/io/zeko/db/sql/MySQLJoinQuerySpec.kt b/src/test/kotlin/io/zeko/db/sql/MySQLJoinQuerySpec.kt
index 7cefdef..ec4cc61 100644
--- a/src/test/kotlin/io/zeko/db/sql/MySQLJoinQuerySpec.kt
+++ b/src/test/kotlin/io/zeko/db/sql/MySQLJoinQuerySpec.kt
@@ -133,6 +133,23 @@ class MySQLJoinQuerySpec : Spek({
"GROUP BY role.id, role.name " +
"HAVING SUM( role.id ) > 2 AND COUNT( role.id ) < 10 ORDER BY user.id ASC LIMIT 10 OFFSET 20", sql)
}
+
+ it("should match sql with one table inner join a subquery") {
+ val sql = Query().fields("*")
+ .from("user")
+ .innerJoin(
+ Query().fields("id", "user_id", "(total_savings - total_spendings) as balance").from("report"),
+ "user_wallet"
+ )
+ .on("user_wallet.user_id = user.id")
+ .toSql()
+ debug(sql)
+ assertEquals("""
+ SELECT * FROM user INNER JOIN (
+ SELECT id, user_id, (total_savings - total_spendings) as balance FROM report ) as user_wallet
+ ON ( user_wallet.user_id = user.id )
+ """.trimIndent().replace("\n", ""), sql)
+ }
}
}
})