Skip to content

Commit

Permalink
refactor: EXPOSED-728 [SQLite] Remove ENABLE_UPDATE_DELETE_LIMIT meta…
Browse files Browse the repository at this point in the history
…data

check from core function provider

SqliteDialect has a companion property that sends a low-level JDBC connection
query to check if LIMIT clause is allowed in either update or delete statements.
In preparation for R2DBC, this metadata query is removed from the core module and
replaced with appropriate DatabaseDialect and ExposedDatabaseMetadata functions that
can be called in appropriate blocking/non-blocking ways.

Tests have been adjusted accordingly.
  • Loading branch information
bog-walk committed Feb 11, 2025
1 parent a32e1f6 commit 69281d7
Show file tree
Hide file tree
Showing 11 changed files with 94 additions and 96 deletions.
5 changes: 5 additions & 0 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -3627,6 +3627,7 @@ public abstract class org/jetbrains/exposed/sql/statements/api/ExposedDatabaseMe
public abstract fun resetCurrentScheme ()V
public abstract fun resolveReferenceOption (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/ReferenceOption;
public abstract fun sequences ()Ljava/util/List;
public abstract fun supportsLimitWithUpdateOrDelete ()Z
public abstract fun tableConstraints (Ljava/util/List;)Ljava/util/Map;
public abstract fun tableNamesByCurrentSchema (Ljava/util/Map;)Lorg/jetbrains/exposed/sql/vendors/SchemaMetadata;
}
Expand Down Expand Up @@ -3922,6 +3923,7 @@ public abstract interface class org/jetbrains/exposed/sql/vendors/DatabaseDialec
public abstract fun sequenceExists (Lorg/jetbrains/exposed/sql/Sequence;)Z
public abstract fun sequences ()Ljava/util/List;
public abstract fun setSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String;
public abstract fun supportsLimitWithUpdateOrDelete ()Z
public abstract fun supportsSelectForUpdate ()Z
public abstract fun tableColumns ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map;
public abstract fun tableExists (Lorg/jetbrains/exposed/sql/Table;)Z
Expand Down Expand Up @@ -4289,6 +4291,7 @@ public class org/jetbrains/exposed/sql/vendors/PostgreSQLDialect : org/jetbrains
public fun listDatabases ()Ljava/lang/String;
public fun modifyColumn (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnDiff;)Ljava/util/List;
public fun setSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String;
public fun supportsLimitWithUpdateOrDelete ()Z
}

public final class org/jetbrains/exposed/sql/vendors/PostgreSQLDialect$Companion : org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider {
Expand Down Expand Up @@ -4355,6 +4358,7 @@ public class org/jetbrains/exposed/sql/vendors/SQLiteDialect : org/jetbrains/exp
public fun getSupportsWindowFrameGroupsMode ()Z
public fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/Expression;)Z
public fun listDatabases ()Ljava/lang/String;
public fun supportsLimitWithUpdateOrDelete ()Z
}

public final class org/jetbrains/exposed/sql/vendors/SQLiteDialect$Companion : org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider {
Expand Down Expand Up @@ -4434,6 +4438,7 @@ public abstract class org/jetbrains/exposed/sql/vendors/VendorDialect : org/jetb
public fun sequenceExists (Lorg/jetbrains/exposed/sql/Sequence;)Z
public fun sequences ()Ljava/util/List;
public fun setSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String;
public fun supportsLimitWithUpdateOrDelete ()Z
public fun supportsSelectForUpdate ()Z
public fun tableColumns ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map;
public fun tableExists (Lorg/jetbrains/exposed/sql/Table;)Z
Expand Down
38 changes: 34 additions & 4 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,26 @@ package org.jetbrains.exposed.sql

import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IdTable
import org.jetbrains.exposed.exceptions.UnsupportedByDialectException
import org.jetbrains.exposed.sql.statements.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.SQLServerDialect
import org.jetbrains.exposed.sql.vendors.currentDialect
import kotlin.collections.ArrayList
import kotlin.collections.Iterable
import kotlin.collections.Iterator
import kotlin.collections.List
import kotlin.collections.emptyList
import kotlin.collections.filter
import kotlin.collections.first
import kotlin.collections.forEach
import kotlin.collections.isNotEmpty
import kotlin.collections.last
import kotlin.collections.listOf
import kotlin.collections.orEmpty
import kotlin.collections.plus
import kotlin.collections.plusAssign
import kotlin.collections.putAll
import kotlin.sequences.Sequence

@Deprecated(
Expand Down Expand Up @@ -121,8 +137,12 @@ fun <T : Table> T.deleteWhere(limit: Int? = null, offset: Long? = null, op: T.(I
inline fun <T : Table> T.deleteWhere(
limit: Int? = null,
op: T.(ISqlExpressionBuilder) -> Op<Boolean>
): Int =
DeleteStatement.where(TransactionManager.current(), this@deleteWhere, op(SqlExpressionBuilder), false, limit)
): Int {
if (limit != null && !currentDialect.supportsLimitWithUpdateOrDelete()) {
throw UnsupportedByDialectException("LIMIT clause is not supported in DELETE statement.", currentDialect)
}
return DeleteStatement.where(TransactionManager.current(), this@deleteWhere, op(SqlExpressionBuilder), false, limit)
}

@Deprecated(
"This `offset` parameter is not being used and will be removed in future releases. Please leave a comment on " +
Expand All @@ -148,8 +168,12 @@ fun <T : Table> T.deleteIgnoreWhere(limit: Int? = null, offset: Long? = null, op
inline fun <T : Table> T.deleteIgnoreWhere(
limit: Int? = null,
op: T.(ISqlExpressionBuilder) -> Op<Boolean>
): Int =
DeleteStatement.where(TransactionManager.current(), this@deleteIgnoreWhere, op(SqlExpressionBuilder), true, limit)
): Int {
if (limit != null && !currentDialect.supportsLimitWithUpdateOrDelete()) {
throw UnsupportedByDialectException("LIMIT clause is not supported in DELETE statement.", currentDialect)
}
return DeleteStatement.where(TransactionManager.current(), this@deleteIgnoreWhere, op(SqlExpressionBuilder), true, limit)
}

/**
* Represents the SQL statement that deletes all rows in a table.
Expand Down Expand Up @@ -588,6 +612,9 @@ inline fun <T : Table> T.update(
limit: Int? = null,
crossinline body: T.(UpdateStatement) -> Unit
): Int {
if (limit != null && !currentDialect.supportsLimitWithUpdateOrDelete()) {
throw UnsupportedByDialectException("LIMIT clause is not supported in UPDATE statement.", currentDialect)
}
val query = UpdateStatement(this, limit, SqlExpressionBuilder.where())
body(query)
return query.execute(TransactionManager.current()) ?: 0
Expand All @@ -604,6 +631,9 @@ inline fun <T : Table> T.update(
limit: Int? = null,
crossinline body: T.(UpdateStatement) -> Unit
): Int {
if (limit != null && !currentDialect.supportsLimitWithUpdateOrDelete()) {
throw UnsupportedByDialectException("LIMIT clause is not supported in UPDATE statement.", currentDialect)
}
val query = UpdateStatement(this, limit, null)
body(query)
return query.execute(TransactionManager.current()) ?: 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ abstract class ExposedDatabaseMetadata(val database: String) {
/** Whether the database supports `SELECT FOR UPDATE` statements. */
abstract val supportsSelectForUpdate: Boolean

/** Whether the database supports the `LIMIT` clause with update and delete statements. */
abstract fun supportsLimitWithUpdateOrDelete(): Boolean

/** Clears and resets any stored information about the database's current schema to default values. */
abstract fun resetCurrentScheme()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ interface DatabaseDialect {
/** Returns the allowed maximum sequence value for a dialect, as a [Long]. */
val sequenceMaxValue: Long get() = Long.MAX_VALUE

/** Returns `true` if the database supports the `LIMIT` clause with update and delete statements. */
fun supportsLimitWithUpdateOrDelete(): Boolean

/** Returns the name of the current database. */
fun getDatabase(): String

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,19 +224,6 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() {
return if (ignore) "$def $ON_CONFLICT_IGNORE" else def
}

override fun update(
target: Table,
columnsAndValues: List<Pair<Column<*>, Any?>>,
limit: Int?,
where: Op<Boolean>?,
transaction: Transaction
): String {
if (limit != null) {
transaction.throwUnsupportedException("PostgreSQL doesn't support LIMIT in UPDATE clause.")
}
return super.update(target, columnsAndValues, null, where, transaction)
}

override fun update(
targets: Join,
columnsAndValues: List<Pair<Column<*>, Any?>>,
Expand Down Expand Up @@ -304,19 +291,6 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() {

override fun insertValue(columnName: String, queryBuilder: QueryBuilder) { queryBuilder { +"EXCLUDED.$columnName" } }

override fun delete(
ignore: Boolean,
table: Table,
where: String?,
limit: Int?,
transaction: Transaction
): String {
if (limit != null) {
transaction.throwUnsupportedException("PostgreSQL doesn't support LIMIT in DELETE clause.")
}
return super.delete(ignore, table, where, null, transaction)
}

override fun delete(
ignore: Boolean,
targets: Join,
Expand Down Expand Up @@ -390,6 +364,8 @@ open class PostgreSQLDialect(override val name: String = dialectName) : VendorDi

override val supportsWindowFrameGroupsMode: Boolean = true

override fun supportsLimitWithUpdateOrDelete(): Boolean = false

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

override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List<String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ package org.jetbrains.exposed.sql.vendors
import org.jetbrains.exposed.exceptions.throwUnsupportedException
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.SQLiteDialect.Companion.ENABLE_UPDATE_DELETE_LIMIT
import java.sql.Connection
import java.sql.DriverManager
import java.sql.ResultSet
import java.sql.Statement

internal object SQLiteDataTypeProvider : DataTypeProvider() {
override fun integerAutoincType(): String = "INTEGER PRIMARY KEY AUTOINCREMENT"
Expand Down Expand Up @@ -201,19 +196,6 @@ internal object SQLiteFunctionProvider : FunctionProvider() {
return if (ignore) def.replaceFirst("INSERT", "INSERT OR IGNORE") else def
}

override fun update(
target: Table,
columnsAndValues: List<Pair<Column<*>, Any?>>,
limit: Int?,
where: Op<Boolean>?,
transaction: Transaction
): String {
if (!ENABLE_UPDATE_DELETE_LIMIT && limit != null) {
transaction.throwUnsupportedException("SQLite doesn't support LIMIT in UPDATE clause.")
}
return super.update(target, columnsAndValues, limit, where, transaction)
}

override fun replace(
table: Table,
columns: List<Column<*>>,
Expand Down Expand Up @@ -258,19 +240,6 @@ internal object SQLiteFunctionProvider : FunctionProvider() {

override fun insertValue(columnName: String, queryBuilder: QueryBuilder) { queryBuilder { +"EXCLUDED.$columnName" } }

override fun delete(
ignore: Boolean,
table: Table,
where: String?,
limit: Int?,
transaction: Transaction
): String {
if (!ENABLE_UPDATE_DELETE_LIMIT && limit != null) {
transaction.throwUnsupportedException("SQLite doesn't support LIMIT in DELETE clause.")
}
return super.delete(ignore, table, where, limit, transaction)
}

override fun queryLimitAndOffset(size: Int?, offset: Long, alreadyOrdered: Boolean): String {
if (size == null && offset > 0) {
TransactionManager.current().throwUnsupportedException("SQLite doesn't support OFFSET clause without LIMIT")
Expand Down Expand Up @@ -309,10 +278,17 @@ internal object SQLiteFunctionProvider : FunctionProvider() {
*/
open class SQLiteDialect : VendorDialect(dialectName, SQLiteDataTypeProvider, SQLiteFunctionProvider) {
override val supportsCreateSequence: Boolean = false

override val supportsMultipleGeneratedKeys: Boolean = false

override val supportsCreateSchema: Boolean = false

override val supportsWindowFrameGroupsMode: Boolean = true

override fun supportsLimitWithUpdateOrDelete(): Boolean {
return TransactionManager.current().db.metadata { supportsLimitWithUpdateOrDelete() }
}

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

override fun createIndex(index: Index): String {
Expand Down Expand Up @@ -341,27 +317,13 @@ open class SQLiteDialect : VendorDialect(dialectName, SQLiteDataTypeProvider, SQ
override fun dropDatabase(name: String) = "DETACH DATABASE ${name.inProperCase()}"

companion object : DialectNameProvider("SQLite") {
val ENABLE_UPDATE_DELETE_LIMIT by lazy {
var conn: Connection? = null
var stmt: Statement? = null
var rs: ResultSet? = null
@Suppress("SwallowedException", "TooGenericExceptionCaught")
try {
conn = DriverManager.getConnection("jdbc:sqlite::memory:")
stmt = conn!!.createStatement()
rs = stmt!!.executeQuery("""SELECT sqlite_compileoption_used("ENABLE_UPDATE_DELETE_LIMIT");""")
if (rs!!.next()) {
rs!!.getBoolean(1)
} else {
false
}
} catch (e: Exception) {
false
} finally {
rs?.close()
stmt?.close()
conn?.close()
}
@Deprecated(
message = "This property will be removed in future releases.",
replaceWith = ReplaceWith("currentDialect.supportsLimitWithUpdateOrDelete()"),
level = DeprecationLevel.WARNING
)
val ENABLE_UPDATE_DELETE_LIMIT: Boolean by lazy {
TransactionManager.current().db.metadata { supportsLimitWithUpdateOrDelete() }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ abstract class VendorDialect(

override val supportsMultipleGeneratedKeys: Boolean = true

override fun supportsLimitWithUpdateOrDelete(): Boolean = true

override fun getDatabase(): String = catalog(TransactionManager.current())

/**
Expand Down
1 change: 1 addition & 0 deletions exposed-jdbc/api/exposed-jdbc.api
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public final class org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadat
public fun resetCurrentScheme ()V
public fun resolveReferenceOption (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/ReferenceOption;
public fun sequences ()Ljava/util/List;
public fun supportsLimitWithUpdateOrDelete ()Z
public fun tableConstraints (Ljava/util/List;)Ljava/util/Map;
public fun tableNamesByCurrentSchema (Ljava/util/Map;)Lorg/jetbrains/exposed/sql/vendors/SchemaMetadata;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import org.jetbrains.exposed.sql.vendors.H2Dialect.H2CompatibilityMode
import java.math.BigDecimal
import java.sql.DatabaseMetaData
import java.sql.ResultSet
import java.sql.SQLException
import java.util.concurrent.ConcurrentHashMap

/**
* Class responsible for retrieving and storing information about the JDBC driver and underlying DBMS, using [metadata].
*/
class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) : ExposedDatabaseMetadata(database) {
override val url: String by lazyMetadata { url }

override val version: BigDecimal by lazyMetadata { BigDecimal("$databaseMajorVersion.$databaseMinorVersion") }

override val majorVersion: Int by lazyMetadata { databaseMajorVersion }

override val minorVersion: Int by lazyMetadata { databaseMinorVersion }

override val databaseDialectName: String by lazyMetadata {
Expand Down Expand Up @@ -69,10 +73,31 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData)
override val defaultIsolationLevel: Int by lazyMetadata { defaultTransactionIsolation }

override val supportsAlterTableWithAddColumn by lazyMetadata { supportsAlterTableWithAddColumn() }

override val supportsAlterTableWithDropColumn by lazyMetadata { supportsAlterTableWithDropColumn() }

override val supportsMultipleResultSets by lazyMetadata { supportsMultipleResultSets() }

override val supportsSelectForUpdate: Boolean by lazyMetadata { supportsSelectForUpdate() }

override fun supportsLimitWithUpdateOrDelete(): Boolean {
return when (currentDialect) {
is SQLiteDialect -> {
try {
val transaction = TransactionManager.current()
transaction.exec("""SELECT sqlite_compileoption_used("ENABLE_UPDATE_DELETE_LIMIT");""") { rs ->
rs.next()
rs.getBoolean(1)
} == true
} catch (_: SQLException) {
false
}
}
is PostgreSQLDialect -> false
else -> true
}
}

override val identifierManager: IdentifierManagerApi by lazyMetadata {
identityManagerCache.getOrPut(url) {
JdbcIdentifierManager(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,11 @@ import org.jetbrains.exposed.sql.tests.shared.assertEquals
import org.jetbrains.exposed.sql.tests.shared.expectException
import org.jetbrains.exposed.sql.vendors.H2Dialect
import org.jetbrains.exposed.sql.vendors.MysqlDialect
import org.jetbrains.exposed.sql.vendors.SQLiteDialect
import org.junit.Test
import kotlin.test.assertTrue
import kotlin.test.expect

class DeleteTests : DatabaseTestsBase() {
private val limitNotSupported by lazy {
val extra = setOf(TestDB.SQLITE).takeUnless { SQLiteDialect.ENABLE_UPDATE_DELETE_LIMIT }.orEmpty()
TestDB.ALL_POSTGRES_LIKE + TestDB.ALL_ORACLE_LIKE + extra
}

@Test
fun testDelete01() {
withCitiesAndUsers { cities, users, userData ->
Expand Down Expand Up @@ -76,7 +70,9 @@ class DeleteTests : DatabaseTestsBase() {
@Test
fun testDeleteWithLimit() {
withCitiesAndUsers { _, _, userData ->
if (currentTestDB in limitNotSupported) {
// EXPOSED-729: Oracle is not currently set up to allow LIMIT syntax, even though it is possible like with UPDATE
// https://youtrack.jetbrains.com/issue/EXPOSED-729/Oracle-Allow-setting-limit-with-DELETE
if (currentTestDB == TestDB.ORACLE || !currentDialectTest.supportsLimitWithUpdateOrDelete()) {
expectException<UnsupportedByDialectException> {
userData.deleteWhere(limit = 1) { userData.value eq 20 }
}
Expand Down
Loading

0 comments on commit 69281d7

Please sign in to comment.