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 1c7b994b35..8502390712 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 @@ -333,32 +333,34 @@ class IntegerColumnType : ColumnType() { /** * Numeric column for storing unsigned 4-byte integers. + * + * **Note:** If the database being used is not MySQL or MariaDB, this column will use the database's + * 8-byte integer type with a check constraint that ensures storage of only values + * between 0 and [UInt.MAX_VALUE] inclusive. */ class UIntegerColumnType : ColumnType() { override fun sqlType(): String = currentDialect.dataTypeProvider.uintegerType() override fun valueFromDB(value: Any): UInt { return when (value) { is UInt -> value - is Int -> value.takeIf { it >= 0 }?.toUInt() - is Number -> value.toLong().takeIf { it >= 0 && it <= UInt.MAX_VALUE.toLong() }?.toUInt() + is Int -> value.toUInt() + is Number -> value.toLong().toUInt() is String -> value.toUInt() else -> error("Unexpected value of type Int: $value of ${value::class.qualifiedName}") - } ?: error("Negative value but type is UInt: $value") + } } override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) { - val v = when { - value is UInt && currentDialect is MysqlDialect -> value.toLong() - value is UInt -> value.toInt() + val v = when (value) { + is UInt -> value.toLong() else -> value } super.setParameter(stmt, index, v) } override fun notNullValueToDB(value: Any): Any { - val v = when { - value is UInt && currentDialect is MysqlDialect -> value.toLong() - value is UInt -> value.toInt() + val v = when (value) { + is UInt -> value.toLong() else -> value } return super.notNullValueToDB(v) 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 a287e4b5b7..003592b164 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 @@ -504,8 +504,15 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { /** Creates a numeric column, with the specified [name], for storing 4-byte integers. */ fun integer(name: String): Column = registerColumn(name, IntegerColumnType()) - /** Creates a numeric column, with the specified [name], for storing 4-byte unsigned integers. */ - fun uinteger(name: String): Column = registerColumn(name, UIntegerColumnType()) + /** Creates a numeric column, with the specified [name], for storing 4-byte unsigned integers. + * + * **Note:** If the database being used is not MySQL or MariaDB, this column will use the database's + * 8-byte integer type with a check constraint that ensures storage of only values + * 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) } + } /** Creates a numeric column, with the specified [name], for storing 8-byte integers. */ fun long(name: String): Column = registerColumn(name, LongColumnType()) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt index e123c9aa50..c00c5851f1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt @@ -39,8 +39,11 @@ abstract class DataTypeProvider { /** Numeric type for storing 4-byte integers. */ open fun integerType(): String = "INT" - /** Numeric type for storing 4-byte unsigned integers. */ - open fun uintegerType(): String = "INT" + /** Numeric type for storing 4-byte unsigned integers. + * + * **Note:** If the database being used is not MySQL or MariaDB, this will represent the 8-byte integer type. + */ + open fun uintegerType(): String = "BIGINT" /** Numeric type for storing 4-byte integers, marked as auto-increment. */ open fun integerAutoincType(): String = "INT AUTO_INCREMENT" diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/UnsignedColumnTypeTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/UnsignedColumnTypeTests.kt index a02944ec64..a7b79c38dc 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/UnsignedColumnTypeTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/UnsignedColumnTypeTests.kt @@ -197,6 +197,71 @@ class UnsignedColumnTypeTests : DatabaseTestsBase() { } } + @Test + fun testUIntWithCheckConstraint() { + withTables(UIntTable) { + val ddlEnding = if (currentDialectTest is MysqlDialect) { + "(uint INT UNSIGNED NOT NULL)" + } else { + "CHECK (uint BETWEEN 0 and ${UInt.MAX_VALUE}))" + } + assertTrue(UIntTable.ddl.single().endsWith(ddlEnding, ignoreCase = true)) + + val number = 3_221_225_471u + assertTrue(number in Int.MAX_VALUE.toUInt()..UInt.MAX_VALUE) + + UIntTable.insert { it[unsignedInt] = number } + + val result = UIntTable.selectAll() + assertEquals(number, result.single()[UIntTable.unsignedInt]) + + // test that column itself blocks same out-of-range value that compiler blocks + assertFailAndRollback("Check constraint violation (or out-of-range error in MySQL/MariaDB)") { + val tableName = UIntTable.nameInDatabaseCase() + val columnName = UIntTable.unsignedInt.nameInDatabaseCase() + val outOfRangeValue = UInt.MAX_VALUE.toLong() + 1L + exec("""INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)""") + } + } + } + + @Test + fun testPreviousUIntColumnTypeWorksWithNewBigIntType() { + // Oracle was already previously constrained to NUMBER(13) + withDb(excludeSettings = listOf(TestDB.MYSQL, TestDB.MARIADB, TestDB.ORACLE)) { testDb -> + try { + val tableName = UIntTable.nameInDatabaseCase() + val columnName = UIntTable.unsignedInt.nameInDatabaseCase() + // create table using previous column type INT + exec("""CREATE TABLE ${addIfNotExistsIfSupported()}$tableName ($columnName INT NOT NULL)""") + + val number1 = Int.MAX_VALUE.toUInt() + UIntTable.insert { it[unsignedInt] = number1 } + + val result1 = UIntTable.select { UIntTable.unsignedInt eq number1 }.count() + assertEquals(1, result1) + + // INT maps to INTEGER in SQLite, so it will not throw OoR error + if (testDb != TestDB.SQLITE) { + val number2 = Int.MAX_VALUE.toUInt() + 1u + assertFailAndRollback("Out-of-range (OoR) error") { + UIntTable.insert { it[unsignedInt] = number2 } + assertEquals(0, UIntTable.select { UIntTable.unsignedInt less 0u }.count()) + } + + // modify column to now have BIGINT type + exec(UIntTable.unsignedInt.modifyStatement().first()) + UIntTable.insert { it[unsignedInt] = number2 } + + val result2 = UIntTable.selectAll().map { it[UIntTable.unsignedInt] } + assertEqualCollections(listOf(number1, number2), result2) + } + } finally { + SchemaUtils.drop(UIntTable) + } + } + } + @Test fun testULongColumnType() { withTables(ULongTable) {