diff --git a/documentation-website/Writerside/topics/Breaking-Changes.md b/documentation-website/Writerside/topics/Breaking-Changes.md index adcd8dd8e3..31379b92c3 100644 --- a/documentation-website/Writerside/topics/Breaking-Changes.md +++ b/documentation-website/Writerside/topics/Breaking-Changes.md @@ -24,6 +24,29 @@ -- Starting from version 0.57.0 INSERT INTO TEST DEFAULT VALUES ``` +* In H2 Oracle, the `long()` column now maps to data type `BIGINT` instead of `NUMBER(19)`. + In Oracle, using the long column in a table now also creates a CHECK constraint to ensure that no out-of-range values are inserted. + Exposed does not ensure this behaviour for SQLite. If you want to do that, please use the following CHECK constraint: + +```kotlin +val long = long("long_column").check { column -> + fun typeOf(value: String) = object : ExpressionWithColumnType() { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("typeof($value)") } + override val columnType: IColumnType = TextColumnType() + } + Expression.build { typeOf(column.name) eq stringLiteral("integer") } +} + +val long = long("long_column").nullable().check { column -> + fun typeOf(value: String) = object : ExpressionWithColumnType() { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("typeof($value)") } + override val columnType: IColumnType = TextColumnType() + } + + val typeCondition = Expression.build { typeOf(column.name) eq stringLiteral("integer") } + column.isNull() or typeCondition +} +``` ## 0.56.0 * If the `distinct` parameter of `groupConcat()` is set to `true`, when using Oracle or SQL Server, this will now fail early with an @@ -44,8 +67,6 @@ that is also type restricted to `Comparable` (for example, `avg()`) will also require defining a new function. In this event, please also leave a comment on [YouTrack](https://youtrack.jetbrains.com/issue/EXPOSED-577) with a use case so the original function signature can be potentially reassessed. -* In H2 Oracle, the `long()` column now maps to data type `BIGINT` instead of `NUMBER(19)`. - In Oracle and SQLite, using the long column in a table now also creates a CHECK constraint to ensure that no out-of-range values are inserted. ## 0.55.0 * The `DeleteStatement` property `table` is now deprecated in favor of `targetsSet`, which holds a `ColumnSet` that may be a `Table` or `Join`. diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt index f12bb357c5..0bd0905090 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt @@ -753,7 +753,9 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { } /** Creates a numeric column, with the specified [name], for storing 8-byte integers. */ - fun long(name: String): Column = registerColumn(name, LongColumnType()) + fun long(name: String): Column = registerColumn(name, LongColumnType()).apply { + check("${generatedSignedCheckPrefix}long_${this.unquotedName()}") { it.between(Long.MIN_VALUE, Long.MAX_VALUE) } + } /** Creates a numeric column, with the specified [name], for storing 8-byte unsigned integers. * @@ -1699,10 +1701,6 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { } append(TransactionManager.current().identity(this@Table)) - // Add CHECK constraint to Long columns in Oracle and SQLite. - // It is done here because special handling is necessary based on the dialect. - addLongColumnCheckConstraintIfNeeded() - if (columns.isNotEmpty()) { columns.joinTo(this, prefix = " (") { column -> column.descriptionDdl(false) @@ -1744,8 +1742,15 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { }.let { if (currentDialect !is SQLiteDialect && currentDialect !is OracleDialect) { it.filterNot { (name, _) -> - name.startsWith("${generatedSignedCheckPrefix}integer") || - name.startsWith("${generatedSignedCheckPrefix}long") + name.startsWith("${generatedSignedCheckPrefix}integer") + } + } else { + it + } + }.let { + if (currentDialect !is OracleDialect) { + it.filterNot { (name, _) -> + name.startsWith("${generatedSignedCheckPrefix}long") } } else { it @@ -1770,34 +1775,6 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { return createAutoIncColumnSequence() + createTable + createConstraint } - private fun addLongColumnCheckConstraintIfNeeded() { - if (currentDialect is OracleDialect || currentDialect is SQLiteDialect) { - columns.filter { it.columnType is LongColumnType }.forEach { column -> - val name = column.name - val checkName = "${generatedSignedCheckPrefix}long_$name" - if (checkConstraints.none { it.first == checkName }) { - column.check(checkName) { - if (currentDialect is SQLiteDialect) { - fun typeOf(value: String) = object : ExpressionWithColumnType() { - override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("typeof($value)") } - override val columnType: IColumnType = TextColumnType() - } - - val typeCondition = Expression.build { typeOf(name) eq stringLiteral("integer") } - if (column.columnType.nullable) { - column.isNull() or typeCondition - } else { - typeCondition - } - } else { - it.between(Long.MIN_VALUE, Long.MAX_VALUE) - } - } - } - } - } - } - private fun createAutoIncColumnSequence(): List { return autoIncColumn?.autoIncColumnType?.sequence?.createStatement().orEmpty() } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/NumericColumnTypesTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/NumericColumnTypesTests.kt index f457456d9e..85fe9726b1 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/NumericColumnTypesTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/NumericColumnTypesTests.kt @@ -117,7 +117,6 @@ class NumericColumnTypesTests : DatabaseTestsBase() { withTables(testTable) { testDb -> val columnName = testTable.long.nameInDatabaseCase() val ddlEnding = when (testDb) { - TestDB.SQLITE -> "CHECK (typeof($columnName) = 'integer'))" TestDB.ORACLE -> "CHECK ($columnName BETWEEN ${Long.MIN_VALUE} and ${Long.MAX_VALUE}))" else -> "($columnName ${testTable.long.columnType} NOT NULL)" } @@ -127,14 +126,18 @@ class NumericColumnTypesTests : DatabaseTestsBase() { testTable.insert { it[long] = Long.MAX_VALUE } assertEquals(2, testTable.select(testTable.long).count()) - val tableName = testTable.nameInDatabaseCase() - assertFailAndRollback(message = "Out-of-range error (or CHECK constraint violation for SQLite & Oracle)") { - val outOfRangeValue = Long.MIN_VALUE.toBigDecimal() - 1.toBigDecimal() - exec("INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)") - } - assertFailAndRollback(message = "Out-of-range error (or CHECK constraint violation for SQLite & Oracle)") { - val outOfRangeValue = Long.MAX_VALUE.toBigDecimal() + 1.toBigDecimal() - exec("INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)") + // SQLite is excluded because it is not possible to enforce the range without a special CHECK constraint + // that the user can implement if they want to + if (testDb != TestDB.SQLITE) { + val tableName = testTable.nameInDatabaseCase() + assertFailAndRollback(message = "Out-of-range error (or CHECK constraint violation for SQLite & Oracle)") { + val outOfRangeValue = Long.MIN_VALUE.toBigDecimal() - 1.toBigDecimal() + exec("INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)") + } + assertFailAndRollback(message = "Out-of-range error (or CHECK constraint violation for SQLite & Oracle)") { + val outOfRangeValue = Long.MAX_VALUE.toBigDecimal() + 1.toBigDecimal() + exec("INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)") + } } } }