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 Jan 23, 2025
1 parent 00a05c7 commit 48198aa
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 56 deletions.
23 changes: 23 additions & 0 deletions documentation-website/Writerside/topics/Breaking-Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<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.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.
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 @@ -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<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 @@ -1704,10 +1706,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 @@ -1749,8 +1747,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 @@ -1775,34 +1780,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 @@ -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(),
Expand All @@ -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)
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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(),
Expand All @@ -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)
Expand All @@ -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(),
Expand All @@ -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)
Expand All @@ -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(),
Expand All @@ -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)
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 48198aa

Please sign in to comment.