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) + } } } })