diff --git a/documentation-website/Writerside/topics/Breaking-Changes.md b/documentation-website/Writerside/topics/Breaking-Changes.md index 02446f93e4..32521c4de5 100644 --- a/documentation-website/Writerside/topics/Breaking-Changes.md +++ b/documentation-website/Writerside/topics/Breaking-Changes.md @@ -13,6 +13,9 @@ The original `FunctionProvider.queryLimit()` is also being deprecated in favor of `queryLimitAndOffset()`, which takes a nullable `size` parameter to allow exclusion of the LIMIT clause. This latter deprecation only affects extensions of the `FunctionProvider` class when creating a custom `VendorDialect` class. +* In Oracle and H2 Oracle, the `short` column now maps to data type `NUMBER(5)` instead of `SMALLINT` because `SMALLINT` is stored as `NUMBER(38)` in the database and + takes up unnecessary storage. + In Oracle, H2 Oracle, and SQLite, using the `short` column in a table also creates a check constraint to ensure that no out-of-range values are inserted. ## 0.54.0 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 b039d28a81..a6b8f540a1 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 @@ -509,7 +509,8 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { private val checkConstraints = mutableListOf>>() - private val generatedCheckPrefix = "chk_${tableName}_unsigned_" + private val generatedUnsignedCheckPrefix = "chk_${tableName}_unsigned_" + private val generatedSignedCheckPrefix = "chk_${tableName}_signed_" /** * Returns the table name in proper case. @@ -696,11 +697,13 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * between 0 and [UByte.MAX_VALUE] inclusive. */ fun ubyte(name: String): Column = registerColumn(name, UByteColumnType()).apply { - check("${generatedCheckPrefix}byte_$name") { it.between(0u, UByte.MAX_VALUE) } + check("${generatedUnsignedCheckPrefix}byte_$name") { it.between(0u, UByte.MAX_VALUE) } } /** Creates a numeric column, with the specified [name], for storing 2-byte integers. */ - fun short(name: String): Column = registerColumn(name, ShortColumnType()) + fun short(name: String): Column = registerColumn(name, ShortColumnType()).apply { + check("${generatedSignedCheckPrefix}short_$name") { it.between(Short.MIN_VALUE, Short.MAX_VALUE) } + } /** Creates a numeric column, with the specified [name], for storing 2-byte unsigned integers. * @@ -708,7 +711,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * integer type with a check constraint that ensures storage of only values between 0 and [UShort.MAX_VALUE] inclusive. */ fun ushort(name: String): Column = registerColumn(name, UShortColumnType()).apply { - check("$generatedCheckPrefix$name") { it.between(0u, UShort.MAX_VALUE) } + check("$generatedUnsignedCheckPrefix$name") { it.between(0u, UShort.MAX_VALUE) } } /** Creates a numeric column, with the specified [name], for storing 4-byte integers. */ @@ -721,7 +724,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * between 0 and [UInt.MAX_VALUE] inclusive. */ fun uinteger(name: String): Column = registerColumn(name, UIntegerColumnType()).apply { - check("$generatedCheckPrefix$name") { it.between(0u, UInt.MAX_VALUE) } + check("$generatedUnsignedCheckPrefix$name") { it.between(0u, UInt.MAX_VALUE) } } /** Creates a numeric column, with the specified [name], for storing 8-byte integers. */ @@ -1641,12 +1644,25 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { } if (checkConstraints.isNotEmpty()) { - val filteredChecks = when (currentDialect) { + val filteredChecks = when (val dialect = currentDialect) { is MysqlDialect -> checkConstraints.filterNot { (name, _) -> - name.startsWith(generatedCheckPrefix) + name.startsWith(generatedUnsignedCheckPrefix) || + name.startsWith(generatedSignedCheckPrefix) } is SQLServerDialect -> checkConstraints.filterNot { (name, _) -> - name.startsWith("${generatedCheckPrefix}byte_") + name.startsWith("${generatedUnsignedCheckPrefix}byte_") || + name.startsWith(generatedSignedCheckPrefix) + } + is PostgreSQLDialect -> checkConstraints.filterNot { (name, _) -> + name.startsWith(generatedSignedCheckPrefix) + } + is H2Dialect -> { + when (dialect.h2Mode) { + H2Dialect.H2CompatibilityMode.Oracle -> checkConstraints + else -> checkConstraints.filterNot { (name, _) -> + name.startsWith(generatedSignedCheckPrefix) + } + } } else -> checkConstraints }.ifEmpty { null } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt index ed41823780..bfb49ab69e 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt @@ -15,6 +15,7 @@ import java.util.* internal object OracleDataTypeProvider : DataTypeProvider() { override fun byteType(): String = "SMALLINT" override fun ubyteType(): String = "NUMBER(4)" + override fun shortType(): String = "NUMBER(5)" override fun ushortType(): String = "NUMBER(6)" override fun integerType(): String = "NUMBER(12)" override fun integerAutoincType(): String = "NUMBER(12)" 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 new file mode 100644 index 0000000000..f4ca7e5a38 --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/NumericColumnTypesTests.kt @@ -0,0 +1,42 @@ +package org.jetbrains.exposed.sql.tests.shared.types + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertFailAndRollback +import org.jetbrains.exposed.sql.tests.shared.assertTrue +import org.junit.Test + +class NumericColumnTypesTests : DatabaseTestsBase() { + @Test + fun testShortAcceptsOnlyAllowedRange() { + val testTable = object : Table("test_table") { + val short = short("short") + } + + withTables(testTable) { testDb -> + val columnName = testTable.short.nameInDatabaseCase() + val ddlEnding = when (testDb) { + TestDB.SQLITE, in TestDB.ALL_ORACLE_LIKE -> "CHECK ($columnName BETWEEN ${Short.MIN_VALUE} and ${Short.MAX_VALUE}))" + else -> "($columnName ${testTable.short.columnType} NOT NULL)" + } + assertTrue(testTable.ddl.single().endsWith(ddlEnding, ignoreCase = true)) + + testTable.insert { it[short] = Short.MIN_VALUE } + testTable.insert { it[short] = Short.MAX_VALUE } + assertEquals(2, testTable.select(testTable.short).count()) + + val tableName = testTable.nameInDatabaseCase() + assertFailAndRollback(message = "Out-of-range error (or CHECK constraint violation for SQLite & Oracle)") { + val outOfRangeValue = Short.MIN_VALUE - 1 + exec("INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)") + } + assertFailAndRollback(message = "Out-of-range error (or CHECK constraint violation for SQLite & Oracle)") { + val outOfRangeValue = Short.MAX_VALUE + 1 + exec("INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)") + } + } + } +}