diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index a8beda4dbc..c87a712321 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -330,6 +330,8 @@ public final class org/jetbrains/exposed/sql/ByteColumnType : org/jetbrains/expo public fun sqlType ()Ljava/lang/String; public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Byte; public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueToDB (Ljava/lang/Byte;)Ljava/lang/Object; + public synthetic fun valueToDB (Ljava/lang/Object;)Ljava/lang/Object; } public final class org/jetbrains/exposed/sql/Case { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt index faeccd0de3..54dacc7b2b 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt @@ -348,6 +348,15 @@ class ByteColumnType : ColumnType() { is String -> value.toByte() else -> error("Unexpected value of type Byte: $value of ${value::class.qualifiedName}") } + + override fun valueToDB(value: Byte?): Any? { + return if (currentDialect is SQLServerDialect) { + // Workaround for SQL Server JDBC driver mysterious error for in-range values if there's a CHECK constraint + value?.toShort() + } else { + super.valueToDB(value) + } + } } /** 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 a6b8f540a1..fd7598962f 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 @@ -688,7 +688,9 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { // Numeric columns /** Creates a numeric column, with the specified [name], for storing 1-byte integers. */ - fun byte(name: String): Column = registerColumn(name, ByteColumnType()) + fun byte(name: String): Column = registerColumn(name, ByteColumnType()).apply { + check("${generatedSignedCheckPrefix}byte_$name") { it.between(Byte.MIN_VALUE, Byte.MAX_VALUE) } + } /** Creates a numeric column, with the specified [name], for storing 1-byte unsigned integers. * @@ -1651,14 +1653,19 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { } is SQLServerDialect -> checkConstraints.filterNot { (name, _) -> name.startsWith("${generatedUnsignedCheckPrefix}byte_") || - name.startsWith(generatedSignedCheckPrefix) + name.startsWith("${generatedSignedCheckPrefix}short") } is PostgreSQLDialect -> checkConstraints.filterNot { (name, _) -> - name.startsWith(generatedSignedCheckPrefix) + name.startsWith("${generatedSignedCheckPrefix}short") } is H2Dialect -> { when (dialect.h2Mode) { - H2Dialect.H2CompatibilityMode.Oracle -> checkConstraints + H2Dialect.H2CompatibilityMode.Oracle -> checkConstraints.filterNot { (name, _) -> + name.startsWith("${generatedSignedCheckPrefix}byte") + } + H2Dialect.H2CompatibilityMode.PostgreSQL -> checkConstraints.filterNot { (name, _) -> + name.startsWith("${generatedSignedCheckPrefix}short") + } else -> checkConstraints.filterNot { (name, _) -> name.startsWith(generatedSignedCheckPrefix) } 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 bfb49ab69e..6da895f8c6 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 @@ -13,7 +13,11 @@ import java.util.* @Suppress("TooManyFunctions") internal object OracleDataTypeProvider : DataTypeProvider() { - override fun byteType(): String = "SMALLINT" + override fun byteType(): String = if (currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) { + "TINYINT" + } else { + "NUMBER(3)" + } override fun ubyteType(): String = "NUMBER(4)" override fun shortType(): String = "NUMBER(5)" override fun ushortType(): String = "NUMBER(6)" diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt index 34a3455364..eb88856759 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt @@ -11,8 +11,14 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager import java.util.* internal object SQLServerDataTypeProvider : DataTypeProvider() { + override fun byteType(): String = if (currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.SQLServer) { + "TINYINT" + } else { + "SMALLINT" + } + override fun ubyteType(): String { - return if ((currentDialect as? H2Dialect)?.h2Mode == H2Dialect.H2CompatibilityMode.SQLServer) { + return if (currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.SQLServer) { "SMALLINT" } else { "TINYINT" @@ -32,7 +38,7 @@ internal object SQLServerDataTypeProvider : DataTypeProvider() { override fun uuidToDB(value: UUID): Any = value.toString() override fun dateTimeType(): String = "DATETIME2" override fun timestampWithTimeZoneType(): String = - if ((currentDialect as? H2Dialect)?.h2Mode == H2Dialect.H2CompatibilityMode.SQLServer) { + if (currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.SQLServer) { "TIMESTAMP(9) WITH TIME ZONE" } else { "DATETIMEOFFSET" 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 f4ca7e5a38..05fa3f3322 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 @@ -39,4 +39,39 @@ class NumericColumnTypesTests : DatabaseTestsBase() { } } } + + @Test + fun testByteAcceptsOnlyAllowedRange() { + val testTable = object : Table("test_table") { + val byte = byte("byte") + } + + withTables(testTable) { testDb -> + val columnName = testTable.byte.nameInDatabaseCase() + val ddlEnding = when (testDb) { + in TestDB.ALL_POSTGRES_LIKE, TestDB.ORACLE, TestDB.SQLITE, TestDB.SQLSERVER -> + "CHECK ($columnName BETWEEN ${Byte.MIN_VALUE} and ${Byte.MAX_VALUE}))" + else -> "($columnName ${testTable.byte.columnType} NOT NULL)" + } + assertTrue(testTable.ddl.single().endsWith(ddlEnding, ignoreCase = true)) + + testTable.insert { it[byte] = Byte.MIN_VALUE } + testTable.insert { it[byte] = Byte.MAX_VALUE } + assertEquals(2, testTable.select(testTable.byte).count()) + + val tableName = testTable.nameInDatabaseCase() + assertFailAndRollback( + message = "CHECK constraint violation or out-of-range error for MySQL, MariaDB, and H2 (except for H2_V2_PSQL)" + ) { + val outOfRangeValue = Byte.MIN_VALUE - 1 + exec("INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)") + } + assertFailAndRollback( + message = "CHECK constraint violation or out-of-range error for MySQL, MariaDB, and H2 (except for H2_V2_PSQL)" + ) { + val outOfRangeValue = Byte.MAX_VALUE + 1 + exec("INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)") + } + } + } }