Skip to content

Commit

Permalink
fix: EXPOSED-178 DELETE_RULE read incorrectly for Oracle
Browse files Browse the repository at this point in the history
Oracle returns CASCADE=0, SET_NULL=2, and NO_ACTION=1
`decode (f.delete_rule, 'CASCADE', 0, 'SET NULL', 2, 1) as delete_rule`

So when it returns 1, which in the current code corresponds to DatabaseMetaData.importedKeyRestrict, it will now be mapped to NO_ACTION because Oracle does not have the RESTRICT reference option. NO_ACTION behaves likes RESTRICT for it.
  • Loading branch information
joc-a committed Sep 25, 2023
1 parent 8550a32 commit 6067a1d
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import org.jetbrains.exposed.sql.vendors.SQLiteDialect
import org.jetbrains.exposed.sql.vendors.currentDialect
import org.jetbrains.exposed.sql.vendors.currentDialectIfAvailable
import org.jetbrains.exposed.sql.vendors.inProperCase
import java.sql.DatabaseMetaData

/**
* Common interface for database objects that can be created, modified and dropped.
Expand Down Expand Up @@ -36,18 +35,6 @@ enum class ReferenceOption {
SET_DEFAULT;

override fun toString(): String = name.replace("_", " ")

companion object {
/** Returns the corresponding [ReferenceOption] for the specified [refOption] from JDBC. */
fun resolveRefOptionFromJdbc(refOption: Int): ReferenceOption = when (refOption) {
DatabaseMetaData.importedKeyCascade -> CASCADE
DatabaseMetaData.importedKeySetNull -> SET_NULL
DatabaseMetaData.importedKeyRestrict -> RESTRICT
DatabaseMetaData.importedKeyNoAction -> NO_ACTION
DatabaseMetaData.importedKeySetDefault -> SET_DEFAULT
else -> currentDialect.defaultReferenceOption
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.jetbrains.exposed.sql.vendors

import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
import java.sql.DatabaseMetaData

/**
* Common interface for all database dialects.
Expand Down Expand Up @@ -145,6 +146,16 @@ interface DatabaseDialect {
}
}

/** Returns the corresponding [ReferenceOption] for the specified [refOption] from JDBC. */
fun resolveRefOptionFromJdbc(refOption: Int): ReferenceOption = when (refOption) {
DatabaseMetaData.importedKeyCascade -> ReferenceOption.CASCADE
DatabaseMetaData.importedKeySetNull -> ReferenceOption.SET_NULL
DatabaseMetaData.importedKeyRestrict -> ReferenceOption.RESTRICT
DatabaseMetaData.importedKeyNoAction -> ReferenceOption.NO_ACTION
DatabaseMetaData.importedKeySetDefault -> ReferenceOption.SET_DEFAULT
else -> currentDialect.defaultReferenceOption
}

companion object {
private val defaultLikePatternSpecialChars = mapOf('%' to null, '_' to null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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 java.sql.DatabaseMetaData
import java.util.*

internal object OracleDataTypeProvider : DataTypeProvider() {
Expand Down Expand Up @@ -366,5 +367,16 @@ open class OracleDialect : VendorDialect(dialectName, OracleDataTypeProvider, Or
}
}

/**
* The SQL that gets the constraint information for Oracle returns a 1 for NO ACTION and does not support RESTRICT.
* `decode (f.delete_rule, 'CASCADE', 0, 'SET NULL', 2, 1) as delete_rule`
*/
override fun resolveRefOptionFromJdbc(refOption: Int): ReferenceOption = when (refOption) {
DatabaseMetaData.importedKeyCascade -> ReferenceOption.CASCADE
DatabaseMetaData.importedKeySetNull -> ReferenceOption.SET_NULL
DatabaseMetaData.importedKeyRestrict -> ReferenceOption.NO_ACTION
else -> currentDialect.defaultReferenceOption
}

companion object : DialectNameProvider("Oracle")
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,9 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData)
identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(it.nameInDatabaseCase()) == targetColumnName
} ?: return@iterate null // Do not crash if there are missing fields in Exposed's tables
val constraintUpdateRule = getObject("UPDATE_RULE")?.toString()?.toIntOrNull()?.let {
ReferenceOption.resolveRefOptionFromJdbc(it)
currentDialect.resolveRefOptionFromJdbc(it)
}
val constraintDeleteRule = ReferenceOption.resolveRefOptionFromJdbc(getInt("DELETE_RULE"))
val constraintDeleteRule = currentDialect.resolveRefOptionFromJdbc(getInt("DELETE_RULE"))
ForeignKeyConstraint(
target = targetColumn,
from = fromColumn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ class ForeignKeyConstraintTests : DatabaseTestsBase() {
}

@Test
fun testUpdateRuleReadCorrectlyWhenNotSpecifiedInChildTable() {
fun testUpdateAndDeleteRulesReadCorrectlyWhenNotSpecifiedInChildTable() {
val category = object : Table("Category") {
val id = integer("id")

Expand All @@ -193,9 +193,14 @@ class ForeignKeyConstraintTests : DatabaseTestsBase() {
constraints.values.forEach { list ->
list.forEach {
when (testDb) {
TestDB.H2_ORACLE, TestDB.H2_SQLSERVER ->
TestDB.H2_ORACLE, TestDB.H2_SQLSERVER -> {
assertEquals(ReferenceOption.RESTRICT, it.updateRule)
else -> assertEquals(currentDialectTest.defaultReferenceOption, it.updateRule)
assertEquals(ReferenceOption.RESTRICT, it.deleteRule)
}
else -> {
assertEquals(currentDialectTest.defaultReferenceOption, it.updateRule)
assertEquals(currentDialectTest.defaultReferenceOption, it.deleteRule)
}
}
}
}
Expand All @@ -204,7 +209,7 @@ class ForeignKeyConstraintTests : DatabaseTestsBase() {
}

@Test
fun testUpdateRuleReadCorrectlyWhenSpecifiedInChildTable() {
fun testUpdateAndDeleteRulesReadCorrectlyWhenSpecifiedInChildTable() {
val category = object : Table("Category") {
val id = integer("id")

Expand All @@ -213,7 +218,12 @@ class ForeignKeyConstraintTests : DatabaseTestsBase() {

val item = object : Table("Item") {
val id = integer("id")
val categoryId = integer("categoryId").references(category.id, onUpdate = ReferenceOption.CASCADE)
val categoryId = integer("categoryId")
.references(
category.id,
onUpdate = ReferenceOption.CASCADE,
onDelete = ReferenceOption.CASCADE
)

override val primaryKey = PrimaryKey(id)
}
Expand All @@ -226,6 +236,7 @@ class ForeignKeyConstraintTests : DatabaseTestsBase() {
constraints.values.forEach { list ->
list.forEach {
assertEquals(ReferenceOption.CASCADE, it.updateRule)
assertEquals(ReferenceOption.CASCADE, it.deleteRule)
}
}
}
Expand Down

0 comments on commit 6067a1d

Please sign in to comment.