Skip to content

Commit

Permalink
Add support for check constraints. (#270)
Browse files Browse the repository at this point in the history
* #268 Add support for check constraints.
* Add support for multi-column check constraints
* Fixed error when parsing check constraints
* Improved CHECK constraint support
- Warning shown when trying to create a CHECK constraint in MySQL
- Constraint with names that were already added are ignored and a warning is shown
- White spaces are now trimmed from constraint names
- Improved tests
* Replaced exception with warn log when using on an unsupported database
  • Loading branch information
SackCastellon authored and Tapac committed Mar 28, 2018
1 parent ad7c308 commit 41d5b30
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 4 deletions.
1 change: 0 additions & 1 deletion src/main/kotlin/org/jetbrains/exposed/sql/Column.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ class Column<T>(val table: Table, val name: String, override val columnType: ICo
append(" NOT NULL")
}


if (isOneColumnPK()) {
append(" PRIMARY KEY")
}
Expand Down
31 changes: 31 additions & 0 deletions src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,37 @@ data class ForeignKeyConstraint(val fkName: String, val refereeTable: String, va

}

data class CheckConstraint(val tableName: String, val checkName: String, val checkOp: String) : DdlAware {

companion object {
fun from(table: Table, name: String, op: Op<Boolean>): CheckConstraint {
val tr = TransactionManager.current()
return CheckConstraint(tr.identity(table), if (name.isBlank()) "" else tr.quoteIfNecessary(name), op.toString())
}
}

internal val checkPart = buildString {
if (checkName.isNotBlank()) append(" CONSTRAINT $checkName")
append(" CHECK ($checkOp)")
}

override fun createStatement(): List<String> {
return if (currentDialect is MysqlDialect) {
exposedLogger.warn("Creation of CHECK constraints is not currently supported by MySQL")
listOf()
} else listOf("ALTER TABLE $tableName ADD$checkPart")
}

override fun dropStatement(): List<String> {
return if (currentDialect is MysqlDialect) {
exposedLogger.warn("Deletion of CHECK constraints is not currently supported by MySQL")
listOf()
} else listOf("ALTER TABLE $tableName DROP CONSTRAINT $checkName")
}

override fun modifyStatement() = dropStatement() + createStatement()
}

data class Index(val columns: List<Column<*>>, val unique: Boolean, val customName: String? = null) : DdlAware {
val table: Table

Expand Down
26 changes: 26 additions & 0 deletions src/main/kotlin/org/jetbrains/exposed/sql/Table.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ open class Table(name: String = ""): ColumnSet(), DdlAware {
override fun describe(s: Transaction): String = s.identity(this)

val indices = ArrayList<Index>()
val checkConstraints = ArrayList<Pair<String, Op<Boolean>>>()

override val fields: List<Expression<*>>
get() = columns
Expand Down Expand Up @@ -418,6 +419,28 @@ open class Table(name: String = ""): ColumnSet(), DdlAware {
index(customIndexName,true, *columns)
}

/**
* Creates a check constraint in this column.
* @param name The name to identify the constraint, optional. Must be **unique** (case-insensitive) to this table, otherwise, the constraint will
* not be created. All names are [trimmed][String.trim], blank names are ignored and the database engine decides the default name.
* @param op The expression against which the newly inserted values will be compared.
*/
fun <T> Column<T>.check(name: String = "", op: SqlExpressionBuilder.(Column<T>) -> Op<Boolean>) = apply {
table.checkConstraints.takeIf { name.isEmpty() || it.none { it.first.equals(name, true) } }?.add(name to SqlExpressionBuilder.op(this))
?: exposedLogger.warn("A CHECK constraint with name '$name' was ignored because there is already one with that name")
}

/**
* Creates a check constraint in this table.
* @param name The name to identify the constraint, optional. Must be **unique** (case-insensitive) to this table, otherwise, the constraint will
* not be created. All names are [trimmed][String.trim], blank names are ignored and the database engine decides the default name.
* @param op The expression against which the newly inserted values will be compared.
*/
fun check(name: String = "", op: SqlExpressionBuilder.() -> Op<Boolean>) {
checkConstraints.takeIf { name.isEmpty() || it.none { it.first.equals(name, true) } }?.add(name to SqlExpressionBuilder.op())
?: exposedLogger.warn("A CHECK constraint with name '$name' was ignored because there is already one with that name")
}

val ddl: List<String>
get() = createStatement()

Expand All @@ -444,6 +467,9 @@ open class Table(name: String = ""): ColumnSet(), DdlAware {
append(references.joinToString(prefix = ", ", separator = ", ") { ForeignKeyConstraint.from(it).foreignKeyPart })
}
}
if (checkConstraints.isNotEmpty()) {
append(checkConstraints.joinToString(prefix = ",", separator = ",") { (name, op) -> CheckConstraint.from(this@Table, name, op).checkPart })
}

append(")")
}
Expand Down
70 changes: 67 additions & 3 deletions src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ class DDLTests : DatabaseTestsBase() {
else -> "NULL"
}


withTables(TestTable) {
val dtType = currentDialect.dataTypeProvider.dateTimeType()
assertEquals("CREATE TABLE " + if (currentDialect.supportsIfNotExists) { "IF NOT EXISTS " } else { "" } +
Expand Down Expand Up @@ -421,11 +421,11 @@ class DDLTests : DatabaseTestsBase() {
}
}


private abstract class EntityTable(name: String = "") : IdTable<String>(name) {
override val id: Column<EntityID<String>> = varchar("id", 64).clientDefault { UUID.randomUUID().toString() }.primaryKey().entityId()
}

@Test fun complexTest01() {
val User = object : EntityTable() {
val name = varchar("name", 255)
Expand Down Expand Up @@ -504,6 +504,70 @@ class DDLTests : DatabaseTestsBase() {
SchemaUtils.drop(missingTable)
}
}

@Test fun testCheckConstraint01() {
val checkTable = object : Table("checkTable") {
val positive = integer("positive").check { it greaterEq 0 }
val negative = integer("negative").check("subZero") { it less 0 }
}

withTables(listOf(TestDB.MYSQL), checkTable) {
checkTable.insert {
it[positive] = 42
it[negative] = -14
}

assertEquals(1, checkTable.selectAll().count())

assertFailAndRollback("Check constraint 1") {
checkTable.insert {
it[positive] = -472
it[negative] = -354
}
}

assertFailAndRollback("Check constraint 2") {
checkTable.insert {
it[positive] = 538
it[negative] = 915
}
}
}
}

@Test fun testCheckConstraint02() {
val checkTable = object : Table("multiCheckTable") {
val positive = integer("positive")
val negative = integer("negative")

init {
check("multi") { (negative less 0) and (positive greaterEq 0) }
}
}

withTables(listOf(TestDB.MYSQL), checkTable) {
checkTable.insert {
it[positive] = 57
it[negative] = -32
}

assertEquals(1, checkTable.selectAll().count())

assertFailAndRollback("Check constraint 1") {
checkTable.insert {
it[positive] = -47
it[negative] = -35
}
}

assertFailAndRollback("Check constraint 2") {
checkTable.insert {
it[positive] = 53
it[negative] = 91
}
}
}
}
}

private fun String.inProperCase(): String = TransactionManager.currentOrNull()?.let { tm ->
Expand Down

0 comments on commit 41d5b30

Please sign in to comment.