diff --git a/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt b/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt index 2602ac2ffa..d1be64ead1 100644 --- a/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt +++ b/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt @@ -85,7 +85,6 @@ class Column(val table: Table, val name: String, override val columnType: ICo append(" NOT NULL") } - if (isOneColumnPK()) { append(" PRIMARY KEY") } diff --git a/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt b/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt index 192da21195..c78282f9fa 100644 --- a/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt +++ b/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt @@ -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): 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 { + 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 { + 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>, val unique: Boolean, val customName: String? = null) : DdlAware { val table: Table diff --git a/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt b/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt index b2c009c09e..91a84331f4 100644 --- a/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt +++ b/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt @@ -154,6 +154,7 @@ open class Table(name: String = ""): ColumnSet(), DdlAware { override fun describe(s: Transaction): String = s.identity(this) val indices = ArrayList() + val checkConstraints = ArrayList>>() override val fields: List> get() = columns @@ -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 Column.check(name: String = "", op: SqlExpressionBuilder.(Column) -> Op) = 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) { + 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 get() = createStatement() @@ -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(")") } diff --git a/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt b/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt index dfff631517..1b84b0219c 100644 --- a/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt +++ b/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt @@ -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 { "" } + @@ -421,11 +421,11 @@ class DDLTests : DatabaseTestsBase() { } } - + private abstract class EntityTable(name: String = "") : IdTable(name) { override val id: Column> = varchar("id", 64).clientDefault { UUID.randomUUID().toString() }.primaryKey().entityId() } - + @Test fun complexTest01() { val User = object : EntityTable() { val name = varchar("name", 255) @@ -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 ->