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

Add support for check constraints. #270

Merged
merged 6 commits into from
Mar 28, 2018
Merged
Show file tree
Hide file tree
Changes from 5 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
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 indexName: String, val table: Table, val columns: List<Column<*>>, val unique: Boolean) : DdlAware {
companion object {
fun forColumns(vararg columns: Column<*>, unique: Boolean): Index {
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<Pair<Array<out Column<*>>, Boolean>>()
val checkConstraints = ArrayList<Pair<String, Op<Boolean>>>()

override val fields: List<Expression<*>>
get() = columns
Expand Down Expand Up @@ -402,6 +403,28 @@ open class Table(name: String = ""): ColumnSet(), DdlAware {
index(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 @@ -428,6 +451,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 @@ -384,11 +384,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 @@ -467,6 +467,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