Skip to content

Commit

Permalink
#287 Update Limit not working with PostgreSQL
Browse files Browse the repository at this point in the history
update/insert/delete/replace functions moved from VendorDialect to FunctionProvider
  • Loading branch information
Tapac committed May 8, 2018
1 parent afe38b6 commit 9ca1bbc
Show file tree
Hide file tree
Showing 18 changed files with 211 additions and 151 deletions.
7 changes: 5 additions & 2 deletions src/main/kotlin/org/jetbrains/exposed/sql/Exceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package org.jetbrains.exposed.exceptions

import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.EntityID
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.statements.StatementContext
import org.jetbrains.exposed.sql.statements.expandArgs
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.VendorDialect
import org.jetbrains.exposed.sql.vendors.DatabaseDialect
import java.sql.SQLException

class EntityNotFoundException(val id: EntityID<*>, val entity: EntityClass<*, *>): Exception("Entity ${entity.klass.simpleName}, id=$id not found in database")
Expand All @@ -25,4 +26,6 @@ class ExposedSQLException(cause: Throwable?, val contexts: List<StatementContext
}
}

class UnsupportedByDialectException(baseMessage: String, dialect: VendorDialect) : UnsupportedOperationException(baseMessage + ", dialect: ${dialect.name}.")
class UnsupportedByDialectException(baseMessage: String, dialect: DatabaseDialect) : UnsupportedOperationException(baseMessage + ", dialect: ${dialect.name}.")

internal fun Transaction.throwUnsupportedException(message: String): Nothing = throw UnsupportedByDialectException(message, db.dialect)
2 changes: 1 addition & 1 deletion src/main/kotlin/org/jetbrains/exposed/sql/Query.kt
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ open class Query(set: FieldSet, where: Op<Boolean>?): SizedIterable<ResultRow>,

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class SQLServerBatchInsertStatement(table: Table, ignore: Boolean = false) : Bat
}
}
}
return transaction.db.dialect.insert(isIgnore, table, values.firstOrNull()?.map { it.first }.orEmpty(), sql, transaction)
return transaction.db.dialect.functionProvider.insert(isIgnore, table, values.firstOrNull()?.map { it.first }.orEmpty(), sql, transaction)
}

override fun arguments() = listOfNotNull(super.arguments().flatten().takeIf { data.isNotEmpty() })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ class DeleteStatement(val table: Table, val where: Op<Boolean>? = null, val isIg
return executeUpdate()
}

override fun prepareSQL(transaction: Transaction): String = transaction.db.dialect.delete(isIgnore, table, where?.toSQL(QueryBuilder(true)), transaction)
override fun prepareSQL(transaction: Transaction): String =
transaction.db.dialect.functionProvider.delete(isIgnore, table, where?.toSQL(QueryBuilder(true)), transaction)

override fun arguments(): Iterable<Iterable<Pair<IColumnType, Any?>>> = QueryBuilder(true).run {
where?.toSQL(this)
listOf(args)
}

companion object {

fun where(transaction: Transaction, table: Table, op: Op<Boolean>, isIgnore: Boolean = false): Int
= DeleteStatement(table, op, isIgnore).execute(transaction) ?: 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ class InsertSelectStatement(val columns: List<Column<*>>, val selectQuery: Query
override fun arguments(): Iterable<Iterable<Pair<IColumnType, Any?>>> = selectQuery.arguments()

override fun prepareSQL(transaction: Transaction): String =
transaction.db.dialect.insert(isIgnore, targets.single(), columns, selectQuery.prepareSQL(transaction), transaction)
transaction.db.dialect.functionProvider.insert(isIgnore, targets.single(), columns, selectQuery.prepareSQL(transaction), transaction)
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ open class InsertStatement<Key:Any>(val table: Table, val isIgnore: Boolean = fa
else values.joinToString(prefix = "VALUES (", postfix = ")") { (col, value) ->
builder.registerArgument(col, value)
}
return transaction.db.dialect.insert(isIgnore, table, values.map { it.first }, sql, transaction)
return transaction.db.dialect.functionProvider.insert(isIgnore, table, values.map { it.first }, sql, transaction)
}

protected open fun PreparedStatement.execInsertFunction() : Pair<Int, ResultSet?> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ package org.jetbrains.exposed.sql.statements

import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.vendors.currentDialect

/**
* @author max
*/
class ReplaceStatement<Key:Any>(table: Table) : InsertStatement<Key>(table) {

override fun prepareSQL(transaction: Transaction): String = currentDialect.replace(table, arguments!!.first(), transaction)
override fun prepareSQL(transaction: Transaction): String = transaction.db.dialect.functionProvider.replace(table, arguments!!.first(), transaction)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,8 @@ open class UpdateStatement(val targetsSet: ColumnSet, val limit: Int?, val where
}
}

override fun prepareSQL(transaction: Transaction): String = buildString {
val builder = QueryBuilder(true)
append("UPDATE ${targetsSet.describe(transaction)}")
append(" SET ")
append(firstDataSet.joinToString { (col, value) ->
"${transaction.identity(col)}=" + builder.registerArgument(col, value)
})

where?.let { append(" WHERE " + it.toSQL(builder)) }
limit?.let { append(" LIMIT $it")}
}

override fun prepareSQL(transaction: Transaction): String =
transaction.db.dialect.functionProvider.update(targetsSet, firstDataSet, limit, where, transaction)

override fun arguments(): Iterable<Iterable<Pair<IColumnType, Any?>>> = QueryBuilder(true).run {
values.forEach {
Expand Down
96 changes: 51 additions & 45 deletions src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.jetbrains.exposed.sql.vendors

import org.jetbrains.exposed.exceptions.UnsupportedByDialectException
import org.jetbrains.exposed.exceptions.throwUnsupportedException
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
import java.nio.ByteBuffer
Expand Down Expand Up @@ -44,7 +44,9 @@ open class DataTypeProvider {
}
}

open class FunctionProvider {
abstract class FunctionProvider {

open val DEFAULT_VALUE_EXPRESSION = "DEFAULT VALUES"

open fun<T:String?> substring(expr: Expression<T>, start: Expression<Int>, length: Expression<Int>, builder: QueryBuilder) : String =
"SUBSTRING(${expr.toSQL(builder)}, ${start.toSQL(builder)}, ${length.toSQL(builder)})"
Expand All @@ -55,6 +57,52 @@ open class FunctionProvider {

open fun<T:String?> ExpressionWithColumnType<T>.match(pattern: String, mode: MatchMode? = null): Op<Boolean> = with(SqlExpressionBuilder) { this@match.like(pattern) }

open fun insert(ignore: Boolean, table: Table, columns: List<Column<*>>, expr: String, transaction: Transaction): String {
if (ignore) {
transaction.throwUnsupportedException("There's no generic SQL for INSERT IGNORE. There must be vendor specific implementation")
}

val (columnsExpr, valuesExpr) = if (columns.isNotEmpty()) {
columns.joinToString(prefix = "(", postfix = ")") { transaction.identity(it) } to expr
} else "" to DEFAULT_VALUE_EXPRESSION

return "INSERT INTO ${transaction.identity(table)} $columnsExpr $valuesExpr"
}

open fun update(targets: ColumnSet, columnsAndValues: List<Pair<Column<*>, Any?>>, limit: Int?, where: Op<Boolean>?, transaction: Transaction): String {
return buildString {
val builder = QueryBuilder(true)
append("UPDATE ${targets.describe(transaction)}")
append(" SET ")
append(columnsAndValues.joinToString { (col, value) ->
"${transaction.identity(col)}=" + builder.registerArgument(col, value)
})

where?.let { append(" WHERE " + it.toSQL(builder)) }
limit?.let { append(" LIMIT $it")}
}
}

open fun delete(ignore: Boolean, table: Table, where: String?, transaction: Transaction): String {
if (ignore) {
transaction.throwUnsupportedException("There's no generic SQL for DELETE IGNORE. There must be vendor specific implementation")
}

return buildString {
append("DELETE FROM ")
append(transaction.identity(table))
if (where != null) {
append(" WHERE ")
append(where)
}
}
}

open fun replace(table: Table, data: List<Pair<Column<*>, Any?>>, transaction: Transaction): String
= transaction.throwUnsupportedException("There's no generic SQL for replace. There must be vendor specific implementation")

open fun queryLimit(size: Int, offset: Int, alreadyOrdered: Boolean) = "LIMIT $size" + if (offset > 0) " OFFSET $offset" else ""

interface MatchMode {
fun mode() : String
}
Expand Down Expand Up @@ -111,20 +159,14 @@ interface DatabaseDialect {

// Specific SQL statements

fun insert(ignore: Boolean, table: Table, columns: List<Column<*>>, expr: String, transaction: Transaction): String
fun delete(ignore: Boolean, table: Table, where: String?, transaction: Transaction): String
fun replace(table: Table, data: List<Pair<Column<*>, Any?>>, transaction: Transaction): String

fun createIndex(index: Index): String
fun dropIndex(tableName: String, indexName: String): String
fun modifyColumn(column: Column<*>) : String

fun limit(size: Int, offset: Int = 0, alreadyOrdered: Boolean = true): String
}

abstract class VendorDialect(override val name: String,
override val dataTypeProvider: DataTypeProvider,
override val functionProvider: FunctionProvider = FunctionProvider()) : DatabaseDialect {
override val functionProvider: FunctionProvider) : DatabaseDialect {

/* Cached values */
private var _allTableNames: List<String>? = null
Expand Down Expand Up @@ -258,39 +300,6 @@ abstract class VendorDialect(override val name: String,
existingIndicesCache.clear()
}

override fun replace(table: Table, data: List<Pair<Column<*>, Any?>>, transaction: Transaction): String {
throwUnsupportedException("There's no generic SQL for replace. There must be vendor specific implementation")
}

protected open val DEFAULT_VALUE_EXPRESSION = "DEFAULT VALUES"

override fun insert(ignore: Boolean, table: Table, columns: List<Column<*>>, expr: String, transaction: Transaction): String {
if (ignore) {
throwUnsupportedException("There's no generic SQL for INSERT IGNORE. There must be vendor specific implementation")
}

val (columnsExpr, valuesExpr) = if (columns.isNotEmpty()) {
columns.joinToString(prefix = "(", postfix = ")") { transaction.identity(it) } to expr
} else "" to DEFAULT_VALUE_EXPRESSION

return "INSERT INTO ${transaction.identity(table)} $columnsExpr $valuesExpr"
}

override fun delete(ignore: Boolean, table: Table, where: String?, transaction: Transaction): String {
if (ignore) {
throwUnsupportedException("There's no generic SQL for DELETE IGNORE. There must be vendor specific implementation")
}

return buildString {
append("DELETE FROM ")
append(transaction.identity(table))
if (where != null) {
append(" WHERE ")
append(where)
}
}
}

override fun createIndex(index: Index): String {
val t = TransactionManager.current()
val quotedTableName = t.identity(index.table)
Expand All @@ -314,11 +323,8 @@ abstract class VendorDialect(override val name: String,

override val supportsMultipleGeneratedKeys: Boolean = true

override fun limit(size: Int, offset: Int, alreadyOrdered: Boolean) = "LIMIT $size" + if (offset > 0) " OFFSET $offset" else ""

override fun modifyColumn(column: Column<*>): String = "MODIFY COLUMN ${column.descriptionDdl()}"

protected fun throwUnsupportedException(message: String): Nothing = throw UnsupportedByDialectException(message, this)
}

internal val currentDialect: DatabaseDialect get() = TransactionManager.current().db.dialect
Expand Down
25 changes: 15 additions & 10 deletions src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.jetbrains.exposed.sql.vendors

import org.h2.engine.Session
import org.h2.jdbc.JdbcConnection
import org.jetbrains.exposed.exceptions.throwUnsupportedException
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
import java.sql.Wrapper
Expand All @@ -10,16 +11,15 @@ internal object H2DataTypeProvider : DataTypeProvider() {
override fun uuidType(): String = "UUID"
}

internal class H2Dialect: VendorDialect(dialectName, H2DataTypeProvider) {
internal object H2FunctionProvider : FunctionProvider() {

override fun isAllowedAsColumnDefault(e: Expression<*>): Boolean = true
private fun currentMode(): String =
((TransactionManager.current().connection as Wrapper).unwrap(JdbcConnection::class.java).session as? Session)?.database?.mode?.name ?: ""

private val isMySQLMode: Boolean get() = currentMode() == "MySQL"

override val supportsMultipleGeneratedKeys: Boolean = false

override fun replace(table: Table, data: List<Pair<Column<*>, Any?>>, transaction: Transaction): String {
if (!isMySQLMode) throwUnsupportedException("REPLACE is only supported in MySQL compatibility mode for H2")
if (!isMySQLMode) transaction.throwUnsupportedException("REPLACE is only supported in MySQL compatibility mode for H2")

val builder = QueryBuilder(true)
val values = data.map { builder.registerArgument(it.first.columnType, it.second) }
Expand All @@ -31,11 +31,6 @@ internal class H2Dialect: VendorDialect(dialectName, H2DataTypeProvider) {
return "INSERT INTO ${transaction.identity(table)} (${preparedValues.joinToString { it.first }}) VALUES (${values.joinToString()}) ON DUPLICATE KEY UPDATE ${preparedValues.joinToString { "${it.first}=${it.second}" }}"
}

private fun currentMode(): String =
((TransactionManager.current().connection as Wrapper).unwrap(JdbcConnection::class.java).session as? Session)?.database?.mode?.name ?: ""

override fun existingIndices(vararg tables: Table): Map<Table, List<Index>> =
super.existingIndices(*tables).mapValues { it.value.filterNot { it.indexName.startsWith("PRIMARY_KEY_") } }.filterValues { it.isNotEmpty() }

override fun insert(ignore: Boolean, table: Table, columns: List<Column<*>>, expr: String, transaction: Transaction): String {
val uniqueIdxCols = table.indices.filter { it.unique }.flatMap { it.columns.toList() }
Expand All @@ -47,6 +42,16 @@ internal class H2Dialect: VendorDialect(dialectName, H2DataTypeProvider) {
super.insert(ignore, table, columns, expr, transaction)
}
}
}

internal class H2Dialect : VendorDialect(dialectName, H2DataTypeProvider, H2FunctionProvider) {

override fun isAllowedAsColumnDefault(e: Expression<*>): Boolean = true

override val supportsMultipleGeneratedKeys: Boolean = false

override fun existingIndices(vararg tables: Table): Map<Table, List<Index>> =
super.existingIndices(*tables).mapValues { it.value.filterNot { it.indexName.startsWith("PRIMARY_KEY_") } }.filterValues { it.isNotEmpty() }

override fun createIndex(index: Index): String {
if (index.columns.any { it.columnType is TextColumnType }) {
Expand Down
38 changes: 19 additions & 19 deletions src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ internal object MysqlFunctionProvider : FunctionProvider() {

override fun <T : String?> ExpressionWithColumnType<T>.match(pattern: String, mode: MatchMode?): Op<Boolean> = MATCH(this, pattern, mode ?: MysqlMatchMode.STRICT)

override fun replace(table: Table, data: List<Pair<Column<*>, Any?>>, transaction: Transaction): String {
val builder = QueryBuilder(true)
val columns = data.joinToString { transaction.identity(it.first) }
val values = data.joinToString { builder.registerArgument(it.first.columnType, it.second) }
return "REPLACE INTO ${transaction.identity(table)} ($columns) VALUES ($values)"
}

override val DEFAULT_VALUE_EXPRESSION: String = "() VALUES ()"

override fun insert(ignore: Boolean, table: Table, columns: List<Column<*>>, expr: String, transaction: Transaction): String {
val def = super.insert(false, table, columns, expr, transaction)
return if (ignore) def.replaceFirst("INSERT", "INSERT IGNORE") else def
}

override fun delete(ignore: Boolean, table: Table, where: String?, transaction: Transaction): String {
val def = super.delete(false, table, where, transaction)
return if (ignore) def.replaceFirst("DELETE", "DELETE IGNORE") else def
}

private class MATCH(val expr: ExpressionWithColumnType<*>, val pattern: String, val mode: MatchMode) : Op<Boolean>() {
override fun toSQL(queryBuilder: QueryBuilder): String =
"MATCH(${expr.toSQL(queryBuilder)}) AGAINST ('$pattern' ${mode.mode()})"
Expand Down Expand Up @@ -88,25 +107,6 @@ internal class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider,
return constraints
}

override fun replace(table: Table, data: List<Pair<Column<*>, Any?>>, transaction: Transaction): String {
val builder = QueryBuilder(true)
val columns = data.joinToString { transaction.identity(it.first) }
val values = data.joinToString { builder.registerArgument(it.first.columnType, it.second) }
return "REPLACE INTO ${transaction.identity(table)} ($columns) VALUES ($values)"
}

override val DEFAULT_VALUE_EXPRESSION: String = "() VALUES ()"

override fun insert(ignore: Boolean, table: Table, columns: List<Column<*>>, expr: String, transaction: Transaction): String {
val def = super.insert(false, table, columns, expr, transaction)
return if (ignore) def.replaceFirst("INSERT", "INSERT IGNORE") else def
}

override fun delete(ignore: Boolean, table: Table, where: String?, transaction: Transaction): String {
val def = super.delete(false, table, where, transaction)
return if (ignore) def.replaceFirst("DELETE", "DELETE IGNORE") else def
}

override fun dropIndex(tableName: String, indexName: String): String =
"ALTER TABLE $tableName DROP INDEX $indexName"

Expand Down
Loading

0 comments on commit 9ca1bbc

Please sign in to comment.