Skip to content

Commit

Permalink
fix!: EXPOSED-150 Auto-quoted column names change case across databases
Browse files Browse the repository at this point in the history
Column and table names that are reserved keywords are automatically quoted before
being used in SQL statements. Databases that support upper case folding (H2, Oracle)
quote and upper case the identifiers, so attempting to use the tables across different
databases fails.

This fix ensures any reserved keywords used as identifiers are only quoted, so they
now retain whatever case the user provides them in, but it will be equivalent
across databases.

This broke some tests that checked for index name, as names like TABLE_column_IDX,
were being created in those databases. This was avoided by pulling inProperCase() out
of the buildString until the end of the name creation.

BREAKING CHANGE: [H2, Oracle] Reserved words will be treated as quoted identifiers
and no longer have their case automatically changed to upper case.
  • Loading branch information
bog-walk committed Aug 18, 2023
1 parent fb94e6a commit 46e1df6
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -259,15 +259,15 @@ data class Index(
get() = customName ?: buildString {
append(table.nameInDatabaseCase())
append('_')
append(columns.joinToString("_") { it.name }.inProperCase())
append(columns.joinToString("_") { it.name })
functions?.let { f ->
if (columns.isNotEmpty()) append('_')
append(f.joinToString("_") { it.toString().substringBefore("(").lowercase() }.inProperCase())
append(f.joinToString("_") { it.toString().substringBefore("(").lowercase() })
}
if (unique) {
append("_unique".inProperCase())
append("_unique")
}
}
}.inProperCase()

init {
require(columns.isNotEmpty() || functions?.isNotEmpty() == true) { "At least one column or function is required to create an index" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ abstract class IdentifierManagerApi {
alreadyQuoted && supportsMixedQuotedIdentifiers -> identity
alreadyQuoted && isUpperCaseQuotedIdentifiers -> identity.uppercase()
alreadyQuoted && isLowerCaseQuotedIdentifiers -> identity.lowercase()
supportsMixedIdentifiers -> identity
supportsMixedIdentifiers || keywords.any { identity.equals(it, true) } -> identity
oracleVersion != OracleVersion.NonOracle -> identity.uppercase()
isUpperCaseIdentifiers -> identity.uppercase()
isLowerCaseIdentifiers -> identity.lowercase()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,46 @@ class DDLTests : DatabaseTestsBase() {
}
}

object KeyWordTable : IntIdTable(name = "keywords") {
val bool = bool("bool")
}
@Test
fun testCreateTableWithKeywordIdentifiers() {
val keywords = listOf("key", "public", "data", "constraint")
val keywordTable = object : Table(keywords[0]) {
val public = bool(keywords[1])
val data = integer(keywords[2])
val constraint = varchar(keywords[3], 32)
}

@Test fun tableExistsWithKeyword() {
withTables(KeyWordTable) {
assertEquals(true, KeyWordTable.exists())
KeyWordTable.insert {
it[bool] = true
withDb { testDb ->
SchemaUtils.create(keywordTable)
assertTrue(keywordTable.exists())

val (tableName, publicName, dataName, constraintName) = keywords.map { name ->
if (testDb in listOf(TestDB.MYSQL, TestDB.MARIADB)) "`$name`" else "\"$name\""
}

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)"
assertEquals(expectedCreate, keywordTable.ddl.single())

// check that insert and select statement identifiers also match in DB without throwing SQLException
keywordTable.insert {
it[public] = false
it[data] = 999
it[constraint] = "unique"
}

val expectedSelect = "SELECT $tableName.$publicName, $tableName.$dataName, $tableName.$constraintName FROM $tableName"
keywordTable.selectAll().also {
assertEquals(expectedSelect, it.prepareSQL(this, prepared = false))
}.single()

// check that identifiers match with returned jdbc metadata
val statements = SchemaUtils.statementsRequiredToActualizeScheme(keywordTable)
assertTrue(statements.isEmpty())

SchemaUtils.drop(keywordTable)
}
}

Expand Down Expand Up @@ -923,8 +953,13 @@ class DDLTests : DatabaseTestsBase() {
}
}

object KeyWordTable : IntIdTable(name = "keywords") {
val bool = bool("bool")
}

// https://github.com/JetBrains/Exposed/issues/112
@Test fun testDropTableFlushesCache() {
@Test
fun testDropTableFlushesCache() {
withDb {
class Keyword(id: EntityID<Int>) : IntEntity(id) {
var bool by KeyWordTable.bool
Expand Down

0 comments on commit 46e1df6

Please sign in to comment.