diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt index 991ecd97ac..adf6495979 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt @@ -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 @@ -29,7 +34,8 @@ enum class ReferenceOption { CASCADE, SET_NULL, RESTRICT, - NO_ACTION; + NO_ACTION, + SET_DEFAULT; override fun toString(): String = name.replace("_", " ") @@ -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 } } @@ -108,6 +115,7 @@ data class ForeignKeyConstraint( from.joinToString("_") { it.name } }__${target.joinToString("_") { it.name }}" ).inProperCase() + internal val foreignKeyPart: String get() = buildString { if (fkName.isNotBlank()) { @@ -115,11 +123,37 @@ data class ForeignKeyConstraint( } 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") } diff --git a/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/shared/ForeignKeyTables.kt b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/shared/ForeignKeyTables.kt new file mode 100644 index 0000000000..f44660754b --- /dev/null +++ b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/shared/ForeignKeyTables.kt @@ -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) +} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt index ea243fbee8..72469e5077 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt @@ -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 @@ -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) } @@ -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") @@ -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") } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/ForeignKeyConstraintTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/ForeignKeyConstraintTests.kt new file mode 100644 index 0000000000..171d524f8b --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/ForeignKeyConstraintTests.kt @@ -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] + ) + } +}