Skip to content

Commit

Permalink
chore: Exclude SQLite from out-of-range check since the raw SQL behav…
Browse files Browse the repository at this point in the history
…iour is that it is not possible to enforce the range without a special CHECK constraint that checks the type
  • Loading branch information
joc-a committed Nov 26, 2024
1 parent d27a577 commit bc28075
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 52 deletions.
25 changes: 23 additions & 2 deletions documentation-website/Writerside/topics/Breaking-Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,29 @@
-- Starting from version 0.57.0
INSERT INTO TEST DEFAULT VALUES
```
* 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<String>() {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("typeof($value)") }
override val columnType: IColumnType<String> = TextColumnType()
}
Expression.build { typeOf(column.name) eq stringLiteral("integer") }
}

val long = long("long_column").nullable().check { column ->
fun typeOf(value: String) = object : ExpressionWithColumnType<String>() {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("typeof($value)") }
override val columnType: IColumnType<String> = TextColumnType()
}

val typeCondition = Expression.build { typeOf(column.name) eq stringLiteral("integer") }
column.isNull() or typeCondition
}
```

## 0.56.0
* If the `distinct` parameter of `groupConcat()` is set to `true`, when using Oracle or SQL Server, this will now fail early with an
Expand All @@ -44,8 +67,6 @@
that is also type restricted to `Comparable` (for example, `avg()`) will also require defining a new function. In this event, please
also leave a comment on [YouTrack](https://youtrack.jetbrains.com/issue/EXPOSED-577) with a use case so the original function signature
can be potentially reassessed.
* In H2 Oracle, the `long()` column now maps to data type `BIGINT` instead of `NUMBER(19)`.
In Oracle and SQLite, using the long 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
47 changes: 12 additions & 35 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,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<Long> = registerColumn(name, LongColumnType())
fun long(name: String): Column<Long> = 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.
*
Expand Down Expand Up @@ -1699,10 +1701,6 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
}
append(TransactionManager.current().identity(this@Table))

// Add CHECK constraint to Long columns in Oracle and SQLite.
// It is done here because special handling is necessary based on the dialect.
addLongColumnCheckConstraintIfNeeded()

if (columns.isNotEmpty()) {
columns.joinTo(this, prefix = " (") { column ->
column.descriptionDdl(false)
Expand Down Expand Up @@ -1744,8 +1742,15 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
}.let {
if (currentDialect !is SQLiteDialect && currentDialect !is OracleDialect) {
it.filterNot { (name, _) ->
name.startsWith("${generatedSignedCheckPrefix}integer") ||
name.startsWith("${generatedSignedCheckPrefix}long")
name.startsWith("${generatedSignedCheckPrefix}integer")
}
} else {
it
}
}.let {
if (currentDialect !is OracleDialect) {
it.filterNot { (name, _) ->
name.startsWith("${generatedSignedCheckPrefix}long")
}
} else {
it
Expand All @@ -1770,34 +1775,6 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
return createAutoIncColumnSequence() + createTable + createConstraint
}

private fun addLongColumnCheckConstraintIfNeeded() {
if (currentDialect is OracleDialect || currentDialect is SQLiteDialect) {
columns.filter { it.columnType is LongColumnType }.forEach { column ->
val name = column.name
val checkName = "${generatedSignedCheckPrefix}long_$name"
if (checkConstraints.none { it.first == checkName }) {
column.check(checkName) {
if (currentDialect is SQLiteDialect) {
fun typeOf(value: String) = object : ExpressionWithColumnType<String>() {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("typeof($value)") }
override val columnType: IColumnType<String> = TextColumnType()
}

val typeCondition = Expression.build { typeOf(name) eq stringLiteral("integer") }
if (column.columnType.nullable) {
column.isNull() or typeCondition
} else {
typeCondition
}
} else {
it.between(Long.MIN_VALUE, Long.MAX_VALUE)
}
}
}
}
}
}

private fun createAutoIncColumnSequence(): List<String> {
return autoIncColumn?.autoIncColumnType?.sequence?.createStatement().orEmpty()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,7 @@ class DefaultsTest : DatabaseTestsBase() {
"${"t10".inProperCase()} $timeType${testTable.t10.constraintNamePart()} ${tLiteral.itOrNull()}" +
when (testDb) {
TestDB.SQLITE ->
", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})" +
", CONSTRAINT chk_t_signed_long_l CHECK (typeof(l) = 'integer')"
", 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})"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,7 @@ class JodaTimeDefaultsTest : DatabaseTestsBase() {
"${"t6".inProperCase()} $timeType${testTable.t6.constraintNamePart()} ${tLiteral.itOrNull()}" +
when (testDb) {
TestDB.SQLITE ->
", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})" +
", CONSTRAINT chk_t_signed_long_l CHECK (typeof(l) = 'integer')"
", 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})"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,7 @@ class DefaultsTest : DatabaseTestsBase() {
"${"t10".inProperCase()} $timeType${testTable.t10.constraintNamePart()} ${tLiteral.itOrNull()}" +
when (testDb) {
TestDB.SQLITE ->
", CONSTRAINT chk_t_signed_integer_id CHECK (${"id".inProperCase()} BETWEEN ${Int.MIN_VALUE} AND ${Int.MAX_VALUE})" +
", CONSTRAINT chk_t_signed_long_l CHECK (typeof(l) = 'integer')"
", 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})"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ class NumericColumnTypesTests : DatabaseTestsBase() {
withTables(testTable) { testDb ->
val columnName = testTable.long.nameInDatabaseCase()
val ddlEnding = when (testDb) {
TestDB.SQLITE -> "CHECK (typeof($columnName) = 'integer'))"
TestDB.ORACLE -> "CHECK ($columnName BETWEEN ${Long.MIN_VALUE} and ${Long.MAX_VALUE}))"
else -> "($columnName ${testTable.long.columnType} NOT NULL)"
}
Expand All @@ -127,14 +126,18 @@ class NumericColumnTypesTests : DatabaseTestsBase() {
testTable.insert { it[long] = Long.MAX_VALUE }
assertEquals(2, testTable.select(testTable.long).count())

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)")
// 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)")
}
}
}
}
Expand Down

0 comments on commit bc28075

Please sign in to comment.