Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: EXPOSED-47 Add support for SET DEFAULT reference option #1744

Merged
merged 5 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package org.jetbrains.exposed.sql

import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.*
import org.jetbrains.exposed.sql.vendors.H2Dialect
import org.jetbrains.exposed.sql.vendors.MariaDBDialect
import org.jetbrains.exposed.sql.vendors.MysqlDialect
import org.jetbrains.exposed.sql.vendors.OracleDialect
import org.jetbrains.exposed.sql.vendors.currentDialect
import org.jetbrains.exposed.sql.vendors.currentDialectIfAvailable
import org.jetbrains.exposed.sql.vendors.h2Mode
import org.jetbrains.exposed.sql.vendors.inProperCase
import java.sql.DatabaseMetaData

Expand All @@ -29,7 +34,8 @@ enum class ReferenceOption {
CASCADE,
SET_NULL,
RESTRICT,
NO_ACTION;
NO_ACTION,
SET_DEFAULT;

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

Expand All @@ -40,6 +46,7 @@ enum class ReferenceOption {
DatabaseMetaData.importedKeySetNull -> SET_NULL
DatabaseMetaData.importedKeyRestrict -> RESTRICT
DatabaseMetaData.importedKeyNoAction -> NO_ACTION
DatabaseMetaData.importedKeySetDefault -> SET_DEFAULT
else -> currentDialect.defaultReferenceOption
}
}
Expand Down Expand Up @@ -108,18 +115,45 @@ data class ForeignKeyConstraint(
from.joinToString("_") { it.name }
}__${target.joinToString("_") { it.name }}"
).inProperCase()

internal val foreignKeyPart: String
get() = buildString {
if (fkName.isNotBlank()) {
append("CONSTRAINT $fkName ")
}
append("FOREIGN KEY ($fromColumns) REFERENCES $targetTableName($targetColumns)")
if (deleteRule != ReferenceOption.NO_ACTION) {
append(" ON DELETE $deleteRule")
if (deleteRule == ReferenceOption.SET_DEFAULT) {
when (currentDialect) {
is MariaDBDialect -> exposedLogger.warn(
"MariaDB doesn't support FOREIGN KEY with SET DEFAULT reference option with ON DELETE clause. " +
"Please check your $fromTableName table."
)
is MysqlDialect -> exposedLogger.warn(
"MySQL doesn't support FOREIGN KEY with SET DEFAULT reference option with ON DELETE clause. " +
"Please check your $fromTableName table."
)
else -> append(" ON DELETE $deleteRule")
}
} else {
append(" ON DELETE $deleteRule")
}
}
if (updateRule != ReferenceOption.NO_ACTION) {
if (currentDialect is OracleDialect || currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) {
exposedLogger.warn("Oracle doesn't support FOREIGN KEY with ON UPDATE clause. Please check your $fromTableName table.")
} else if (updateRule == ReferenceOption.SET_DEFAULT) {
when (currentDialect) {
is MariaDBDialect -> exposedLogger.warn(
"MariaDB doesn't support FOREIGN KEY with SET DEFAULT reference option with ON UPDATE clause. " +
"Please check your $fromTableName table."
)
is MysqlDialect -> exposedLogger.warn(
"MySQL doesn't support FOREIGN KEY with SET DEFAULT reference option with ON UPDATE clause. " +
"Please check your $fromTableName table."
)
else -> append(" ON UPDATE $updateRule")
}
} else {
append(" ON UPDATE $updateRule")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.jetbrains.exposed.sql.tests.shared

import org.jetbrains.exposed.sql.ReferenceOption
import org.jetbrains.exposed.sql.Table

object Category : Table("Category") {
val id = integer("id")
val name = varchar(name = "name", length = 20)

override val primaryKey = PrimaryKey(id)
}

const val DEFAULT_CATEGORY_ID = 0

object Item : Table("Item") {
val id = integer("id")
val name = varchar(name = "name", length = 20)
val categoryId = integer("categoryId")
.default(DEFAULT_CATEGORY_ID)
.references(
Category.id,
onDelete = ReferenceOption.SET_DEFAULT,
onUpdate = ReferenceOption.NO_ACTION
)

override val primaryKey = PrimaryKey(id)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
import org.jetbrains.exposed.sql.tests.TestDB
import org.jetbrains.exposed.sql.tests.currentDialectTest
import org.jetbrains.exposed.sql.tests.inProperCase
import org.jetbrains.exposed.sql.tests.shared.Category
import org.jetbrains.exposed.sql.tests.shared.Item
import org.jetbrains.exposed.sql.tests.shared.assertEqualCollections
import org.jetbrains.exposed.sql.tests.shared.assertEquals
import org.jetbrains.exposed.sql.tests.shared.assertTrue
Expand Down Expand Up @@ -533,6 +535,7 @@ class CreateTableTests : DatabaseTestsBase() {
val parent = object : Table("parent2") {
val idA = integer("id_a")
val idB = integer("id_b")

init {
uniqueIndex(idA, idB)
}
Expand Down Expand Up @@ -572,6 +575,23 @@ class CreateTableTests : DatabaseTestsBase() {
}
}

@Test
fun createTableWithOnDeleteSetDefault() {
withDb(excludeSettings = listOf(TestDB.MARIADB, TestDB.MYSQL)) {
val expected = listOf(
"CREATE TABLE " + addIfNotExistsIfSupported() + "${this.identity(Item)} (" +
"${Item.columns.joinToString { it.descriptionDdl(false) }}," +
" CONSTRAINT ${"fk_Item_categoryId__id".inProperCase()}" +
" FOREIGN KEY (${this.identity(Item.categoryId)})" +
" REFERENCES ${this.identity(Category)}(${this.identity(Category.id)})" +
" ON DELETE SET DEFAULT" +
")"
)

assertEqualCollections(Item.ddl, expected)
}
}

object OneTable : IntIdTable("one")
object OneOneTable : IntIdTable("one.one")

Expand All @@ -598,7 +618,8 @@ class CreateTableTests : DatabaseTestsBase() {
}
}

@Test fun `create table with quoted name with camel case`() {
@Test
fun `create table with quoted name with camel case`() {
val testTable = object : IntIdTable("quotedTable") {
val int = integer("intColumn")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.jetbrains.exposed.sql.tests.sqlite

import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
import org.jetbrains.exposed.sql.tests.TestDB
import org.jetbrains.exposed.sql.tests.shared.Category
import org.jetbrains.exposed.sql.tests.shared.DEFAULT_CATEGORY_ID
import org.jetbrains.exposed.sql.tests.shared.Item
import org.jetbrains.exposed.sql.tests.shared.assertEquals
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.Assume
import org.junit.Test

class ForeignKeyConstraintTests : DatabaseTestsBase() {

@Test
fun `test ON DELETE SET DEFAULT for databases that support it without SQLite`() {
withDb(excludeSettings = listOf(TestDB.MARIADB, TestDB.MYSQL, TestDB.SQLITE)) {
testOnDeleteSetDefault()
}
}

@Test
fun `test ON DELETE SET DEFAULT for SQLite`() {
Assume.assumeTrue(TestDB.SQLITE in TestDB.enabledInTests())

transaction(Database.connect("jdbc:sqlite:file:test?mode=memory&cache=shared&foreign_keys=on", user = "root", driver = "org.sqlite.JDBC")) {
testOnDeleteSetDefault()
}
}

private fun Transaction.testOnDeleteSetDefault() {
SchemaUtils.create(Category, Item)

Category.insert {
it[id] = DEFAULT_CATEGORY_ID
it[name] = "Default"
}

val saladsId = 1
Category.insert {
it[id] = saladsId
it[name] = "Salads"
}

val tabboulehId = 0
Item.insert {
it[id] = tabboulehId
it[name] = "Tabbouleh"
it[categoryId] = saladsId
}

assertEquals(
saladsId,
Item.select { Item.id eq tabboulehId }.single().also {
println("SELECT result = $it")
}[Item.categoryId]
)

Category.deleteWhere { id eq saladsId }

assertEquals(
DEFAULT_CATEGORY_ID,
Item.select { Item.id eq tabboulehId }.single()[Item.categoryId]
)
}
}