diff --git a/documentation-website/Writerside/topics/Breaking-Changes.md b/documentation-website/Writerside/topics/Breaking-Changes.md index ec483b51f9..919fdc4071 100644 --- a/documentation-website/Writerside/topics/Breaking-Changes.md +++ b/documentation-website/Writerside/topics/Breaking-Changes.md @@ -5,6 +5,29 @@ mapped to an Exposed table object. Now it only checks against database sequences that have a relational dependency on any of the specified tables (for example, any sequence automatically associated with a `SERIAL` column registered to `IdTable`). An unbound sequence created manually via the `CREATE SEQUENCE` command will no longer be checked and will not generate a `DROP` statement. +* 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.57.0 * Insert, Upsert, and Replace statements will no longer implicitly send all default values (except for client-side default values) in every SQL request. 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 9878bdd692..529fab4933 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 @@ -758,7 +758,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. * @@ -1703,6 +1705,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { append("IF NOT EXISTS ") } append(TransactionManager.current().identity(this@Table)) + if (columns.isNotEmpty()) { columns.joinTo(this, prefix = " (") { column -> column.descriptionDdl(false) @@ -1749,6 +1752,14 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { } else { it } + }.let { + if (currentDialect !is OracleDialect) { + it.filterNot { (name, _) -> + name.startsWith("${generatedSignedCheckPrefix}long") + } + } else { + it + } }.ifEmpty { null } filteredChecks?.mapIndexed { index, (name, op) -> val resolvedName = name.ifBlank { "check_${tableNameWithSchemaSanitized}_$index" } 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 64de7bc0c0..b6bda586f8 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 @@ -33,8 +33,12 @@ internal object OracleDataTypeProvider : DataTypeProvider() { override fun integerAutoincType(): String = integerType() override fun uintegerType(): String = "NUMBER(10)" override fun uintegerAutoincType(): String = "NUMBER(10)" - override fun longType(): String = "NUMBER(19)" - override fun longAutoincType(): String = "NUMBER(19)" + override fun longType(): String = if (currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) { + "BIGINT" + } else { + "NUMBER(19)" + } + override fun longAutoincType(): String = longType() override fun ulongType(): String = "NUMBER(20)" override fun ulongAutoincType(): String = "NUMBER(20)" override fun varcharType(colLength: Int): String = "VARCHAR2($colLength CHAR)" diff --git a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/DefaultsTest.kt b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/DefaultsTest.kt index 2ed097d4af..f88ddd9f2d 100644 --- a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/DefaultsTest.kt +++ b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/DefaultsTest.kt @@ -273,8 +273,11 @@ class DefaultsTest : DatabaseTestsBase() { "${"t9".inProperCase()} $timeType${testTable.t9.constraintNamePart()} ${tLiteral.itOrNull()}, " + "${"t10".inProperCase()} $timeType${testTable.t10.constraintNamePart()} ${tLiteral.itOrNull()}" + when (testDb) { - TestDB.SQLITE, TestDB.ORACLE -> + TestDB.SQLITE -> ", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})" + TestDB.ORACLE -> + ", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})" + + ", CONSTRAINT chk_t_signed_long_l CHECK (L BETWEEN ${Long.MIN_VALUE} AND ${Long.MAX_VALUE})" else -> "" } + ")" diff --git a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt index 407cda1494..a2c16e69cd 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt @@ -210,8 +210,11 @@ class JodaTimeDefaultsTest : DatabaseTestsBase() { "${"t5".inProperCase()} $timeType${testTable.t5.constraintNamePart()} ${tLiteral.itOrNull()}, " + "${"t6".inProperCase()} $timeType${testTable.t6.constraintNamePart()} ${tLiteral.itOrNull()}" + when (testDb) { - TestDB.SQLITE, TestDB.ORACLE -> + TestDB.SQLITE -> ", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})" + TestDB.ORACLE -> + ", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})" + + ", CONSTRAINT chk_t_signed_long_l CHECK (L BETWEEN ${Long.MIN_VALUE} AND ${Long.MAX_VALUE})" else -> "" } + ")" diff --git a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt index 22dcbf87e0..b46b607536 100644 --- a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt +++ b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt @@ -271,8 +271,11 @@ class DefaultsTest : DatabaseTestsBase() { "${"t9".inProperCase()} $timeType${testTable.t9.constraintNamePart()} ${tLiteral.itOrNull()}, " + "${"t10".inProperCase()} $timeType${testTable.t10.constraintNamePart()} ${tLiteral.itOrNull()}" + when (testDb) { - TestDB.SQLITE, TestDB.ORACLE -> + TestDB.SQLITE -> ", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})" + TestDB.ORACLE -> + ", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})" + + ", CONSTRAINT chk_t_signed_long_l CHECK (L BETWEEN ${Long.MIN_VALUE} AND ${Long.MAX_VALUE})" else -> "" } + ")" diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt index b19da9bc77..aec2e7cf70 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt @@ -322,7 +322,7 @@ class CreateTableTests : DatabaseTestsBase() { fkName = fkName ) } - withDb { + withDb { testDb -> val t = TransactionManager.current() val expected = listOfNotNull( child.autoIncColumn?.autoIncColumnType?.sequence?.createStatement()?.single(), @@ -331,6 +331,11 @@ class CreateTableTests : DatabaseTestsBase() { " CONSTRAINT ${t.db.identifierManager.cutIfNecessaryAndQuote(fkName).inProperCase()}" + " FOREIGN KEY (${t.identity(child.parentId)})" + " REFERENCES ${t.identity(parent)}(${t.identity(parent.id)})" + + if (testDb == TestDB.ORACLE) { + ", CONSTRAINT chk_child1_signed_long_id CHECK (${this.identity(parent.id)} BETWEEN ${Long.MIN_VALUE} AND ${Long.MAX_VALUE})" + } else { + "" + } + ")" ) assertEqualCollections(child.ddl, expected) @@ -348,12 +353,17 @@ class CreateTableTests : DatabaseTestsBase() { onDelete = ReferenceOption.NO_ACTION, ) } - withDb { + withDb { testDb -> val expected = "CREATE TABLE " + addIfNotExistsIfSupported() + "${this.identity(child)} (" + "${child.columns.joinToString { it.descriptionDdl(false) }}," + " CONSTRAINT ${"fk_Child_parent_id__id".inProperCase()}" + " FOREIGN KEY (${this.identity(child.parentId)})" + " REFERENCES ${this.identity(parent)}(${this.identity(parent.id)})" + + if (testDb == TestDB.ORACLE) { + ", CONSTRAINT chk_Child_signed_long_id CHECK (${this.identity(parent.id)} BETWEEN ${Long.MIN_VALUE} AND ${Long.MAX_VALUE})" + } else { + "" + } + ")" assertEquals(child.ddl.last(), expected) } @@ -370,12 +380,17 @@ class CreateTableTests : DatabaseTestsBase() { onDelete = ReferenceOption.NO_ACTION, ) } - withDb { + withDb { testDb -> val expected = "CREATE TABLE " + addIfNotExistsIfSupported() + "${this.identity(child)} (" + "${child.columns.joinToString { it.descriptionDdl(false) }}," + " CONSTRAINT ${"fk_Child2_parent_id__id".inProperCase()}" + " FOREIGN KEY (${this.identity(child.parentId)})" + " REFERENCES ${this.identity(parent)}(${this.identity(parent.id)})" + + if (testDb == TestDB.ORACLE) { + ", CONSTRAINT chk_Child2_signed_long_id CHECK (${this.identity(parent.id)} BETWEEN ${Long.MIN_VALUE} AND ${Long.MAX_VALUE})" + } else { + "" + } + ")" assertEquals(child.ddl.last(), expected) } @@ -396,7 +411,7 @@ class CreateTableTests : DatabaseTestsBase() { fkName = fkName ) } - withDb { + withDb { testDb -> val t = TransactionManager.current() val expected = listOfNotNull( child.autoIncColumn?.autoIncColumnType?.sequence?.createStatement()?.single(), @@ -405,6 +420,11 @@ class CreateTableTests : DatabaseTestsBase() { " CONSTRAINT ${t.db.identifierManager.cutIfNecessaryAndQuote(fkName).inProperCase()}" + " FOREIGN KEY (${t.identity(child.parentId)})" + " REFERENCES ${t.identity(parent)}(${t.identity(parent.uniqueId)})" + + if (testDb == TestDB.ORACLE) { + ", CONSTRAINT chk_child2_signed_long_id CHECK (${this.identity(parent.id)} BETWEEN ${Long.MIN_VALUE} AND ${Long.MAX_VALUE})" + } else { + "" + } + ")" ) assertEqualCollections(child.ddl, expected) @@ -424,7 +444,7 @@ class CreateTableTests : DatabaseTestsBase() { fkName = fkName ) } - withDb { + withDb { testDb -> val t = TransactionManager.current() val expected = listOfNotNull( child.autoIncColumn?.autoIncColumnType?.sequence?.createStatement()?.single(), @@ -433,6 +453,11 @@ class CreateTableTests : DatabaseTestsBase() { " CONSTRAINT ${t.db.identifierManager.cutIfNecessaryAndQuote(fkName).inProperCase()}" + " FOREIGN KEY (${t.identity(child.parentId)})" + " REFERENCES ${t.identity(parent)}(${t.identity(parent.id)})" + + if (testDb == TestDB.ORACLE) { + ", CONSTRAINT chk_child3_signed_long_id CHECK (${this.identity(parent.id)} BETWEEN ${Long.MIN_VALUE} AND ${Long.MAX_VALUE})" + } else { + "" + } + ")" ) assertEqualCollections(child.ddl, expected) @@ -455,7 +480,7 @@ class CreateTableTests : DatabaseTestsBase() { fkName = fkName ) } - withDb { + withDb { testDb -> val t = TransactionManager.current() val expected = listOfNotNull( child.autoIncColumn?.autoIncColumnType?.sequence?.createStatement()?.single(), @@ -464,6 +489,11 @@ class CreateTableTests : DatabaseTestsBase() { " CONSTRAINT ${t.db.identifierManager.cutIfNecessaryAndQuote(fkName).inProperCase()}" + " FOREIGN KEY (${t.identity(child.parentId)})" + " REFERENCES ${t.identity(parent)}(${t.identity(parent.uniqueId)})" + + if (testDb == TestDB.ORACLE) { + ", CONSTRAINT chk_child4_signed_long_id CHECK (${this.identity(parent.id)} BETWEEN ${Long.MIN_VALUE} AND ${Long.MAX_VALUE})" + } else { + "" + } + ")" ) assertEqualCollections(child.ddl, expected) 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 99b7714721..b8f33444dc 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 @@ -108,6 +108,40 @@ class NumericColumnTypesTests : DatabaseTestsBase() { } } + @Test + fun testLongAcceptsOnlyAllowedRange() { + val testTable = object : Table("test_table") { + val long = long("long_column") + } + + withTables(testTable) { testDb -> + val columnName = testTable.long.nameInDatabaseCase() + val ddlEnding = when (testDb) { + TestDB.ORACLE -> "CHECK ($columnName BETWEEN ${Long.MIN_VALUE} and ${Long.MAX_VALUE}))" + else -> "($columnName ${testTable.long.columnType} NOT NULL)" + } + assertTrue(testTable.ddl.single().endsWith(ddlEnding, ignoreCase = true)) + + testTable.insert { it[long] = Long.MIN_VALUE } + testTable.insert { it[long] = Long.MAX_VALUE } + assertEquals(2, testTable.select(testTable.long).count()) + + // 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)") + } + } + } + } + @Test fun testParams() { val testTable = object : Table("test_table") {