Skip to content

Commit

Permalink
Add set operations #402 / Intersect and Except
Browse files Browse the repository at this point in the history
  • Loading branch information
Tapac committed Sep 19, 2021
1 parent 2cbc808 commit 10a3ca7
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ abstract class AbstractQuery<T : AbstractQuery<T>>(targets: List<Table>) : Sized

var orderByExpressions: List<Pair<Expression<*>, SortOrder>> = mutableListOf()
private set
var distinct: Boolean = false
protected set

var limit: Int? = null
protected set
var offset: Long = 0
Expand All @@ -23,7 +22,6 @@ abstract class AbstractQuery<T : AbstractQuery<T>>(targets: List<Table>) : Sized

protected fun copyTo(other: AbstractQuery<T>) {
other.orderByExpressions = orderByExpressions.toMutableList()
other.distinct = distinct
other.limit = limit
other.offset = offset
other.fetchSize = fetchSize
Expand All @@ -38,9 +36,7 @@ abstract class AbstractQuery<T : AbstractQuery<T>>(targets: List<Table>) : Sized
if (it.args.isNotEmpty()) listOf(it.args) else emptyList()
}

fun withDistinct(value: Boolean = true): T = apply {
distinct = value
} as T
abstract fun withDistinct(value: Boolean = true): T

override fun limit(n: Int, offset: Long): T = apply {
limit = n
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ enum class SortOrder(val code: String) {
}

open class Query(override var set: FieldSet, where: Op<Boolean>?) : AbstractQuery<Query>(set.source.targetTables()) {
var distinct: Boolean = false
protected set

var groupedByColumns: List<Expression<*>> = mutableListOf()
private set
Expand All @@ -39,6 +41,7 @@ open class Query(override var set: FieldSet, where: Op<Boolean>?) : AbstractQuer

override fun copy(): Query = Query(set, where).also { copy ->
copyTo(copy)
copy.distinct = distinct
copy.groupedByColumns = groupedByColumns.toMutableList()
copy.having = having
copy.forUpdate = forUpdate
Expand All @@ -49,6 +52,10 @@ open class Query(override var set: FieldSet, where: Op<Boolean>?) : AbstractQuer
return this
}

override fun withDistinct(value: Boolean): Query = apply {
distinct = value
}

override fun notForUpdate(): Query {
forUpdate = false
return this
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package org.jetbrains.exposed.sql

import org.jetbrains.exposed.exceptions.UnsupportedByDialectException
import org.jetbrains.exposed.sql.statements.Statement
import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi
import org.jetbrains.exposed.sql.vendors.MariaDBDialect
import org.jetbrains.exposed.sql.vendors.MysqlDialect
import org.jetbrains.exposed.sql.vendors.currentDialect
import java.sql.ResultSet

sealed class SetOperation(
val operationName: String,
val firstStatement: AbstractQuery<*>,
val secondStatement: AbstractQuery<*>
) : AbstractQuery<SetOperation>((firstStatement.targets + secondStatement.targets).distinct()) {
val rawStatements: List<AbstractQuery<*>> = listOf(firstStatement, secondStatement)
init {
require(rawStatements.isNotEmpty()) { "$operationName is empty" }
require(rawStatements.none { it is Query && it.isForUpdate() }) { "FOR UPDATE is not allowed within $operationName" }
require(rawStatements.map { it.set.realFields.size }.distinct().size == 1) {
"Each $operationName query must have the same number of columns"
}
if (!currentDialect.supportsSubqueryUnions) {
require(rawStatements.none { (it as AbstractQuery<*>).let { q -> q.orderByExpressions.isNotEmpty() || q.limit != null } }) {
"$operationName may not contain subqueries"
}
}
}

override val set: FieldSet = firstStatement.set

override val queryToExecute: Statement<ResultSet> = this

override fun count(): Long {
try {
count = true
return transaction.exec(this) { rs ->
rs.next()
rs.getLong(1).also {
rs.close()
}
}!!
} finally {
count = false
}
}

override fun empty(): Boolean {
val oldLimit = limit
try {
limit = 1
val rs = transaction.exec(this)!!
return !rs.next().also { rs.close() }
} finally {
limit = oldLimit
}
}

override fun PreparedStatementApi.executeInternal(transaction: Transaction): ResultSet = executeQuery()

override fun prepareSQL(builder: QueryBuilder): String {
builder {
if (count) append("SELECT COUNT(*) FROM (")

prepareStatementSQL(this)

if (orderByExpressions.isNotEmpty()) {
append(" ORDER BY ")
orderByExpressions.appendTo {
append((it.first as? ExpressionAlias<*>)?.alias ?: it.first, " ", it.second.name)
}
}

limit?.let {
append(" ")
append(currentDialect.functionProvider.queryLimit(it, offset, true))
}

if (count) append(") subquery")
}
return builder.toString()
}

protected open fun prepareStatementSQL(builder: QueryBuilder) {
builder {
rawStatements.appendTo(separator = " $operationName ") {
when (it) {
is Query -> {
val isSubQuery = it.orderByExpressions.isNotEmpty() || it.limit != null
if (isSubQuery) append("(")
it.prepareSQL(this)
if (isSubQuery) append(")")
}
is SetOperation -> it.prepareSQL(this)
}
}
}
}
}

class Union(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery<*>) : SetOperation("UNION", firstStatement, secondStatement) {
override fun withDistinct(value: Boolean): SetOperation {
return if (!value) {
UnionAll(firstStatement, secondStatement).also {
copyTo(it)
}
} else {
this
}
}

override fun copy() = Union(firstStatement, secondStatement).also {
copyTo(it)
}
}

class UnionAll(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery<*>) : SetOperation("UNION ALL", firstStatement, secondStatement) {

override fun withDistinct(value: Boolean): SetOperation {
return if (value) {
Union(firstStatement, secondStatement)
} else {
this
}
}

override fun copy() = UnionAll(firstStatement, secondStatement).also {
copyTo(it)
}
}

class Intersect(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery<*>) : SetOperation("INTERSECT", firstStatement, secondStatement) {
override fun copy() = Intersect(firstStatement, secondStatement).also {
copyTo(it)
}

override fun withDistinct(value: Boolean): SetOperation = this

override fun prepareStatementSQL(builder: QueryBuilder) {
if (currentDialect is MysqlDialect && currentDialect !is MariaDBDialect) {
throw UnsupportedByDialectException("$operationName is unsupported", currentDialect)
} else {
super.prepareStatementSQL(builder)
}
}
}

class Except(firstStatement: AbstractQuery<*>, secondStatement: AbstractQuery<*>) : SetOperation("EXCEPT", firstStatement, secondStatement) {
override fun copy() = Intersect(firstStatement, secondStatement).also {
copyTo(it)
}

override fun withDistinct(value: Boolean): SetOperation = this

override fun prepareStatementSQL(builder: QueryBuilder) {
if (currentDialect is MysqlDialect && currentDialect !is MariaDBDialect) {
throw UnsupportedByDialectException("$operationName is unsupported", currentDialect)
} else {
super.prepareStatementSQL(builder)
}
}
}

fun AbstractQuery<*>.union(other: Query): Union = Union(this, other)

fun AbstractQuery<*>.unionAll(other: Query): UnionAll = UnionAll(this, other)

fun AbstractQuery<*>.intersect(other: Query): Intersect = Intersect(this, other)

fun AbstractQuery<*>.except(other: Query): Except = Except(this, other)
107 changes: 0 additions & 107 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Union.kt

This file was deleted.

Loading

0 comments on commit 10a3ca7

Please sign in to comment.