Skip to content

Commit

Permalink
chore!: Change Oracle and H2 Oracle integerType and integerAutoincTyp…
Browse files Browse the repository at this point in the history
…e from NUMBER(12) to NUMBER(10) and INTEGER respectively and add CHECK constraint in SQLite (#2270)
  • Loading branch information
joc-a authored Oct 11, 2024
1 parent 9697dbc commit 58d96f0
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 26 deletions.
2 changes: 2 additions & 0 deletions documentation-website/Writerside/topics/Breaking-Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* In Oracle and H2 Oracle, the `ubyte()` column now maps to data type `NUMBER(3)` instead of `NUMBER(4)`.
* In Oracle and H2 Oracle, the `ushort()` column now maps to data type `NUMBER(5)` instead of `NUMBER(6)`.
* In Oracle and H2 Oracle, the `uinteger()` column now maps to data type `NUMBER(10)` instead of `NUMBER(13)`.
* In Oracle and H2 Oracle, the `integer()` column now maps to data type `NUMBER(10)` and `INTEGER` respectively, instead of `NUMBER(12)`.
In Oracle and SQLite, using the integer 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`.
Expand Down
43 changes: 34 additions & 9 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ class Join(
*
* @param name Table name, by default name will be resolved from a class name with "Table" suffix removed (if present)
*/
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass")
open class Table(name: String = "") : ColumnSet(), DdlAware {
/** Returns the table name. */
open val tableName: String = when {
Expand Down Expand Up @@ -509,8 +509,21 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {

private val checkConstraints = mutableListOf<Pair<String, Op<Boolean>>>()

private val generatedUnsignedCheckPrefix = "chk_${tableName}_unsigned_"
private val generatedSignedCheckPrefix = "chk_${tableName}_signed_"
private val generatedUnsignedCheckPrefix
get() = "chk_${
tableName.replace("\"", "")
.replace("'", "")
.replace("`", "")
.replace('.', '_')
}_unsigned_"

private val generatedSignedCheckPrefix
get() = "chk_${
tableName.replace("\"", "")
.replace("'", "")
.replace("`", "")
.replace('.', '_')
}_signed_"

/**
* Returns the table name in proper case.
Expand Down Expand Up @@ -689,7 +702,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {

/** Creates a numeric column, with the specified [name], for storing 1-byte integers. */
fun byte(name: String): Column<Byte> = registerColumn(name, ByteColumnType()).apply {
check("${generatedSignedCheckPrefix}byte_$name") { it.between(Byte.MIN_VALUE, Byte.MAX_VALUE) }
check("${generatedSignedCheckPrefix}byte_${this.unquotedName()}") { it.between(Byte.MIN_VALUE, Byte.MAX_VALUE) }
}

/** Creates a numeric column, with the specified [name], for storing 1-byte unsigned integers.
Expand All @@ -699,12 +712,12 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
* between 0 and [UByte.MAX_VALUE] inclusive.
*/
fun ubyte(name: String): Column<UByte> = registerColumn(name, UByteColumnType()).apply {
check("${generatedUnsignedCheckPrefix}byte_$name") { it.between(0u, UByte.MAX_VALUE) }
check("${generatedUnsignedCheckPrefix}byte_${this.unquotedName()}") { it.between(0u, UByte.MAX_VALUE) }
}

/** Creates a numeric column, with the specified [name], for storing 2-byte integers. */
fun short(name: String): Column<Short> = registerColumn(name, ShortColumnType()).apply {
check("${generatedSignedCheckPrefix}short_$name") { it.between(Short.MIN_VALUE, Short.MAX_VALUE) }
check("${generatedSignedCheckPrefix}short_${this.unquotedName()}") { it.between(Short.MIN_VALUE, Short.MAX_VALUE) }
}

/** Creates a numeric column, with the specified [name], for storing 2-byte unsigned integers.
Expand All @@ -713,11 +726,13 @@ 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<UShort> = registerColumn(name, UShortColumnType()).apply {
check("$generatedUnsignedCheckPrefix$name") { it.between(0u, UShort.MAX_VALUE) }
check("$generatedUnsignedCheckPrefix${this.unquotedName()}") { it.between(0u, UShort.MAX_VALUE) }
}

/** Creates a numeric column, with the specified [name], for storing 4-byte integers. */
fun integer(name: String): Column<Int> = registerColumn(name, IntegerColumnType())
fun integer(name: String): Column<Int> = registerColumn(name, IntegerColumnType()).apply {
check("${generatedSignedCheckPrefix}integer_${this.unquotedName()}") { it.between(Int.MIN_VALUE, Int.MAX_VALUE) }
}

/** Creates a numeric column, with the specified [name], for storing 4-byte unsigned integers.
*
Expand All @@ -726,7 +741,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
* between 0 and [UInt.MAX_VALUE] inclusive.
*/
fun uinteger(name: String): Column<UInt> = registerColumn(name, UIntegerColumnType()).apply {
check("$generatedUnsignedCheckPrefix$name") { it.between(0u, UInt.MAX_VALUE) }
check("$generatedUnsignedCheckPrefix${this.unquotedName()}") { it.between(0u, UInt.MAX_VALUE) }
}

/** Creates a numeric column, with the specified [name], for storing 8-byte integers. */
Expand Down Expand Up @@ -1680,6 +1695,14 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
}
}
else -> checkConstraints
}.let {
if (currentDialect !is SQLiteDialect && currentDialect !is OracleDialect) {
it.filterNot { (name, _) ->
name.startsWith("${generatedSignedCheckPrefix}integer")
}
} else {
it
}
}.ifEmpty { null }
filteredChecks?.mapIndexed { index, (name, op) ->
val resolvedName = name.ifBlank { "check_${tableName}_$index" }
Expand Down Expand Up @@ -1764,3 +1787,5 @@ internal fun fallbackSequenceName(tableName: String, columnName: String): String
val q = if (tableName.contains('.')) "\"" else ""
return "$q${tableName.replace("\"", "")}_${columnName}_seq$q"
}

private fun <T> Column<T>.unquotedName() = name.trim('\"', '\'', '`')
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ internal object OracleDataTypeProvider : DataTypeProvider() {
"NUMBER(5)"
}
override fun ushortType(): String = "NUMBER(5)"
override fun integerType(): String = "NUMBER(12)"
override fun integerAutoincType(): String = "NUMBER(12)"
override fun integerType(): String = if (currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) {
"INTEGER"
} else {
"NUMBER(10)"
}
override fun integerAutoincType(): String = integerType()
override fun uintegerType(): String = "NUMBER(10)"
override fun uintegerAutoincType(): String = "NUMBER(10)"
override fun longType(): String = "NUMBER(19)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ class DefaultsTest : DatabaseTestsBase() {
"${"t8".inProperCase()} $longType${testTable.t8.constraintNamePart()} ${durLiteral.itOrNull()}, " +
"${"t9".inProperCase()} $timeType${testTable.t9.constraintNamePart()} ${tLiteral.itOrNull()}, " +
"${"t10".inProperCase()} $timeType${testTable.t10.constraintNamePart()} ${tLiteral.itOrNull()}" +
when (testDb) {
TestDB.SQLITE, TestDB.ORACLE ->
", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})"
else -> ""
} +
")"

val expected = if (currentDialectTest is OracleDialect || currentDialectTest.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) {
Expand Down Expand Up @@ -427,6 +432,11 @@ class DefaultsTest : DatabaseTestsBase() {
"${"t1".inProperCase()} $timestampWithTimeZoneType${testTable.t1.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}, " +
"${"t2".inProperCase()} $timestampWithTimeZoneType${testTable.t2.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}, " +
"${"t3".inProperCase()} $timestampWithTimeZoneType${testTable.t3.constraintNamePart()} ${CurrentTimestampWithTimeZone.itOrNull()}" +
when (testDb) {
TestDB.SQLITE, TestDB.ORACLE ->
", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})"
else -> ""
} +
")"

val expected = if (currentDialectTest is OracleDialect ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ class JodaTimeDefaultsTest : DatabaseTestsBase() {
"${"t4".inProperCase()} $dType${testTable.t4.constraintNamePart()} ${dLiteral.itOrNull()}, " +
"${"t5".inProperCase()} $timeType${testTable.t5.constraintNamePart()} ${tLiteral.itOrNull()}, " +
"${"t6".inProperCase()} $timeType${testTable.t6.constraintNamePart()} ${tLiteral.itOrNull()}" +
when (testDb) {
TestDB.SQLITE, TestDB.ORACLE ->
", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})"
else -> ""
} +
")"

val expected = if (currentDialectTest is OracleDialect || currentDialectTest.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) {
Expand Down Expand Up @@ -413,6 +418,11 @@ class JodaTimeDefaultsTest : DatabaseTestsBase() {
"${"t1".inProperCase()} $timestampWithTimeZoneType${testTable.t1.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}, " +
"${"t2".inProperCase()} $timestampWithTimeZoneType${testTable.t2.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}, " +
"${"t3".inProperCase()} $timestampWithTimeZoneType${testTable.t3.constraintNamePart()} ${CurrentDateTime.itOrNull()}" +
when (testDb) {
TestDB.SQLITE, TestDB.ORACLE ->
", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})"
else -> ""
} +
")"

val expected = if (currentDialectTest is OracleDialect ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ class DefaultsTest : DatabaseTestsBase() {
"${"t8".inProperCase()} $longType${testTable.t8.constraintNamePart()} ${durLiteral.itOrNull()}, " +
"${"t9".inProperCase()} $timeType${testTable.t9.constraintNamePart()} ${tLiteral.itOrNull()}, " +
"${"t10".inProperCase()} $timeType${testTable.t10.constraintNamePart()} ${tLiteral.itOrNull()}" +
when (testDb) {
TestDB.SQLITE, TestDB.ORACLE ->
", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})"
else -> ""
} +
")"

val expected = if (currentDialectTest is OracleDialect || currentDialectTest.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) {
Expand Down Expand Up @@ -430,6 +435,11 @@ class DefaultsTest : DatabaseTestsBase() {
"${"t1".inProperCase()} $timestampWithTimeZoneType${testTable.t1.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}, " +
"${"t2".inProperCase()} $timestampWithTimeZoneType${testTable.t2.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}, " +
"${"t3".inProperCase()} $timestampWithTimeZoneType${testTable.t3.constraintNamePart()} ${CurrentTimestampWithTimeZone.itOrNull()}" +
when (testDb) {
TestDB.SQLITE, TestDB.ORACLE ->
", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})"
else -> ""
} +
")"

val expected = if (currentDialectTest is OracleDialect ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class DDLTests : DatabaseTestsBase() {
val constraint = varchar(keywords[3], 32)
}

withDb {
withDb { testDb ->
assertTrue(db.config.preserveKeywordCasing)

SchemaUtils.create(keywordTable)
Expand All @@ -134,7 +134,13 @@ class DDLTests : DatabaseTestsBase() {
val expectedCreate = "CREATE TABLE ${addIfNotExistsIfSupported()}$tableName (" +
"$publicName ${keywordTable.public.columnType.sqlType()} NOT NULL, " +
"$dataName ${keywordTable.data.columnType.sqlType()} NOT NULL, " +
"$constraintName ${keywordTable.constraint.columnType.sqlType()} NOT NULL)"
"$constraintName ${keywordTable.constraint.columnType.sqlType()} NOT NULL" +
when (testDb) {
TestDB.SQLITE, TestDB.ORACLE ->
""", CONSTRAINT chk_data_signed_integer_key CHECK ("key" BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})"""
else -> ""
} +
")"
assertEquals(expectedCreate, keywordTable.ddl.single())

// check that insert and select statement identifiers also match in DB without throwing SQLException
Expand Down Expand Up @@ -167,7 +173,7 @@ class DDLTests : DatabaseTestsBase() {

@Test
fun unnamedTableWithQuotesSQL() {
withTables(excludeSettings = listOf(TestDB.SQLITE), tables = arrayOf(unnamedTable)) {
withTables(excludeSettings = listOf(TestDB.SQLITE), tables = arrayOf(unnamedTable)) { testDb ->
val q = db.identifierManager.quoteString
val tableName = if (currentDialectTest.needsQuotesWhenSymbolsInNames) {
"$q${"unnamedTable$1".inProperCase()}$q"
Expand All @@ -178,7 +184,12 @@ class DDLTests : DatabaseTestsBase() {
val varCharType = currentDialectTest.dataTypeProvider.varcharType(42)
assertEquals(
"CREATE TABLE " + addIfNotExistsIfSupported() + "$tableName " +
"(${"id".inProperCase()} $integerType PRIMARY KEY, $q${"name".inProperCase()}$q $varCharType NOT NULL)",
"(${"id".inProperCase()} $integerType PRIMARY KEY, $q${"name".inProperCase()}$q $varCharType NOT NULL" +
when (testDb) {
TestDB.ORACLE -> ", CONSTRAINT chk_unnamedTable$1_signed_integer_id CHECK (ID BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})"
else -> ""
} +
")",
unnamedTable.ddl
)
}
Expand All @@ -197,7 +208,8 @@ class DDLTests : DatabaseTestsBase() {
val varCharType = currentDialectTest.dataTypeProvider.varcharType(42)
assertEquals(
"CREATE TABLE " + addIfNotExistsIfSupported() + "$tableName " +
"(${"id".inProperCase()} $integerType NOT NULL PRIMARY KEY, $q${"name".inProperCase()}$q $varCharType NOT NULL)",
"(${"id".inProperCase()} $integerType NOT NULL PRIMARY KEY, $q${"name".inProperCase()}$q $varCharType NOT NULL," +
""" CONSTRAINT "chk_unnamedTable$1_signed_integer_id" CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE}))""",
unnamedTable.ddl
)
}
Expand Down Expand Up @@ -244,16 +256,22 @@ class DDLTests : DatabaseTestsBase() {
override val primaryKey = PrimaryKey(id, name)
}

withTables(excludeSettings = listOf(TestDB.MYSQL_V5, TestDB.SQLITE), tables = arrayOf(testTable)) {
withTables(excludeSettings = listOf(TestDB.MYSQL_V5, TestDB.SQLITE), tables = arrayOf(testTable)) { testDb ->
val q = db.identifierManager.quoteString
val varCharType = currentDialectTest.dataTypeProvider.varcharType(42)
val tableDescription = "CREATE TABLE " + addIfNotExistsIfSupported() + "with_different_column_types".inProperCase()
val idDescription = "${"id".inProperCase()} ${currentDialectTest.dataTypeProvider.integerType()}"
val nameDescription = "$q${"name".inProperCase()}$q $varCharType"
val ageDescription = "${"age".inProperCase()} ${db.dialect.dataTypeProvider.integerType()} NULL"
val constraint = "CONSTRAINT pk_with_different_column_types PRIMARY KEY (${"id".inProperCase()}, $q${"name".inProperCase()}$q)"
val primaryKeyConstraint = "CONSTRAINT pk_with_different_column_types PRIMARY KEY (${"id".inProperCase()}, $q${"name".inProperCase()}$q)"
val checkConstraint = when (testDb) {
TestDB.ORACLE ->
", CONSTRAINT chk_with_different_column_types_signed_integer_id CHECK (ID BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})" +
", CONSTRAINT chk_with_different_column_types_signed_integer_age CHECK (AGE BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})"
else -> ""
}

assertEquals("$tableDescription ($idDescription, $nameDescription, $ageDescription, $constraint)", testTable.ddl)
assertEquals("$tableDescription ($idDescription, $nameDescription, $ageDescription, $primaryKeyConstraint$checkConstraint)", testTable.ddl)
}
}

Expand All @@ -276,7 +294,12 @@ class DDLTests : DatabaseTestsBase() {
val ageDescription = "${"age".inProperCase()} ${db.dialect.dataTypeProvider.integerType()} NULL"
val constraint = "CONSTRAINT pk_with_different_column_types PRIMARY KEY (${"id".inProperCase()}, $q${"name".inProperCase()}$q)"

assertEquals("$tableDescription ($idDescription, $nameDescription, $ageDescription, $constraint)", testTable.ddl)
assertEquals(
"$tableDescription ($idDescription, $nameDescription, $ageDescription, $constraint," +
" CONSTRAINT chk_with_different_column_types_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})," +
" CONSTRAINT chk_with_different_column_types_signed_integer_age CHECK (${"age".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE}))",
testTable.ddl
)
}
}

Expand Down
Loading

0 comments on commit 58d96f0

Please sign in to comment.