From 0d6de42bdb2b0f13a22729ad27c5b40e0ca04c2c Mon Sep 17 00:00:00 2001 From: Alexey Soshin Date: Tue, 31 Jan 2023 16:37:09 +0000 Subject: [PATCH 01/10] Rebase from main --- .../jetbrains/exposed/sql/vendors/Default.kt | 2 +- .../jdbc/JdbcDatabaseMetadataImpl.kt | 41 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt index 0b08d6e386..5800624d12 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt @@ -1328,5 +1328,5 @@ internal val currentDialectIfAvailable: DatabaseDialect? null } -internal fun String.inProperCase(): String = +fun String.inProperCase(): String = TransactionManager.currentOrNull()?.db?.identifierManager?.inProperCase(this@inProperCase) ?: this diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt index 6cead460ee..3938f29249 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt @@ -145,26 +145,31 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) return result } + /** + * For each table, returns metadata for each of its columns + */ override fun columns(vararg tables: Table): Map> { - val rs = metadata.getColumns(databaseName, currentScheme, "%", "%") - val result = rs.extractColumns(tables) { - // @see java.sql.DatabaseMetaData.getColumns - // That read should go first as Oracle driver closes connection after that - val defaultDbValue = it.getString("COLUMN_DEF")?.let { sanitizedDefault(it) } - val autoIncrement = it.getString("IS_AUTOINCREMENT") == "YES" - val type = it.getInt("DATA_TYPE") - val columnMetadata = ColumnMetadata( - it.getString("COLUMN_NAME"), - type, - it.getBoolean("NULLABLE"), - it.getInt("COLUMN_SIZE").takeIf { it != 0 }, - autoIncrement, - // Not sure this filters enough but I dont think we ever want to have sequences here - defaultDbValue?.takeIf { !autoIncrement }, - ) - it.getString("TABLE_NAME") to columnMetadata + val columnsMetadata = metadata.getColumns(databaseName, null, "%", "%") + val result = columnsMetadata.use { resultSet -> + resultSet.extractColumns(tables) { + // @see java.sql.DatabaseMetaData.getColumns + // That read should go first as Oracle driver closes connection after that + val defaultDbValue = it.getString("COLUMN_DEF")?.let { sanitizedDefault(it) } + val autoIncrement = it.getString("IS_AUTOINCREMENT") == "YES" + val type = it.getInt("DATA_TYPE") + val columnMetadata = ColumnMetadata( + it.getString("COLUMN_NAME"), + type, + it.getBoolean("NULLABLE"), + it.getInt("COLUMN_SIZE").takeIf { it != 0 }, + autoIncrement, + // Not sure this filters enough but I dont think we ever want to have sequences here + defaultDbValue?.takeIf { !autoIncrement }, + ) + it.getString("TABLE_NAME") to columnMetadata + } } - rs.close() + return result } From bb4896732031956c29cbe0141d567cf31b5e72c8 Mon Sep 17 00:00:00 2001 From: Alexey Soshin Date: Tue, 31 Jan 2023 16:37:09 +0000 Subject: [PATCH 02/10] Rebase from main --- .../sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt index 7d3f7be669..b4a5a49ff7 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt @@ -3,6 +3,8 @@ package org.jetbrains.exposed.sql.tests.shared.ddl import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IdTable import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.dao.id.LongIdTable +import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull import org.jetbrains.exposed.sql.tests.DatabaseTestsBase From 41919854c9c744ad6d6a1b59c7f6671e35260c0d Mon Sep 17 00:00:00 2001 From: Alexey Soshin Date: Wed, 1 Feb 2023 08:55:00 +0000 Subject: [PATCH 03/10] Formatting --- .../ddl/CreateMissingTablesAndColumnsTests.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt index b4a5a49ff7..14b9473e88 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt @@ -296,7 +296,7 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { withDb { testDb -> try { // MySQL doesn't support default values on text columns, hence excluded - table = if(testDb != TestDB.MYSQL) { + table = if (testDb != TestDB.MYSQL) { object : Table("varchar_test") { val varchar = varchar("varchar_column", 255).default(" ") val text = text("text_column").default(" ") @@ -330,7 +330,7 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { @Test fun `columns with default values that are whitespaces shouldn't be treated as empty strings`() { - val tableWhitespaceDefaultVarchar = StringFieldTable("varchar_whitespace_test", false," ") + val tableWhitespaceDefaultVarchar = StringFieldTable("varchar_whitespace_test", false, " ") val tableWhitespaceDefaultText = StringFieldTable("text_whitespace_test", true, " ") @@ -541,7 +541,8 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { } } - @Test fun testCreateTableWithReferenceMultipleTimes() { + @Test + fun testCreateTableWithReferenceMultipleTimes() { withTables(PlayerTable, SessionsTable) { SchemaUtils.createMissingTablesAndColumns(PlayerTable, SessionsTable) SchemaUtils.createMissingTablesAndColumns(PlayerTable, SessionsTable) @@ -556,7 +557,8 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { val playerId = integer("player_id").references(PlayerTable.id) } - @Test fun createTableWithReservedIdentifierInColumnName() { + @Test + fun createTableWithReservedIdentifierInColumnName() { withDb(TestDB.MYSQL) { SchemaUtils.createMissingTablesAndColumns(T1, T2) SchemaUtils.createMissingTablesAndColumns(T1, T2) @@ -569,11 +571,13 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { object ExplicitTable : IntIdTable() { val playerId = integer("player_id").references(PlayerTable.id, fkName = "Explicit_FK_NAME") } + object NonExplicitTable : IntIdTable() { val playerId = integer("player_id").references(PlayerTable.id) } - @Test fun explicitFkNameIsExplicit() { + @Test + fun explicitFkNameIsExplicit() { withTables(ExplicitTable, NonExplicitTable) { assertEquals("Explicit_FK_NAME", ExplicitTable.playerId.foreignKey!!.customFkName) assertEquals(null, NonExplicitTable.playerId.foreignKey!!.customFkName) @@ -584,6 +588,7 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { val name = integer("name").uniqueIndex() val tmp = varchar("temp", 255) } + object T2 : Table("CHAIN") { val ref = integer("ref").references(T1.name) } From f3853ee5ad894316d894349a7632de69befb3a87 Mon Sep 17 00:00:00 2001 From: Alexey Soshin Date: Wed, 1 Feb 2023 11:31:13 +0000 Subject: [PATCH 04/10] Handle MySQL treating schemas as databases --- .../jdbc/JdbcDatabaseMetadataImpl.kt | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt index 3938f29249..2c1d61bbe7 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt @@ -149,25 +149,37 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) * For each table, returns metadata for each of its columns */ override fun columns(vararg tables: Table): Map> { - val columnsMetadata = metadata.getColumns(databaseName, null, "%", "%") - val result = columnsMetadata.use { resultSet -> - resultSet.extractColumns(tables) { - // @see java.sql.DatabaseMetaData.getColumns - // That read should go first as Oracle driver closes connection after that - val defaultDbValue = it.getString("COLUMN_DEF")?.let { sanitizedDefault(it) } - val autoIncrement = it.getString("IS_AUTOINCREMENT") == "YES" - val type = it.getInt("DATA_TYPE") - val columnMetadata = ColumnMetadata( - it.getString("COLUMN_NAME"), - type, - it.getBoolean("NULLABLE"), - it.getInt("COLUMN_SIZE").takeIf { it != 0 }, - autoIncrement, - // Not sure this filters enough but I dont think we ever want to have sequences here - defaultDbValue?.takeIf { !autoIncrement }, - ) - it.getString("TABLE_NAME") to columnMetadata - } + val databases = mutableListOf(databaseName) + + // For MySQL, table names that contain schema are considered to be in their own DB + // For example, if you connect to testdb, but your table is my_schema.test_table, + // the metadata you need to query is for my_schema, not testdb + if (currentDialect is MysqlDialect) { + databases.addAll(extractSchemas(tables)) + } + + val result = mutableMapOf>() + for (database in databases) { + val columnsMetadata = metadata.getColumns(database, null, "%", "%") + result += (columnsMetadata.use { resultSet -> + resultSet.extractColumns(tables) { + // @see java.sql.DatabaseMetaData.getColumns + // That read should go first as Oracle driver closes connection after that + val defaultDbValue = it.getString("COLUMN_DEF")?.let { sanitizedDefault(it) } + val autoIncrement = it.getString("IS_AUTOINCREMENT") == "YES" + val type = it.getInt("DATA_TYPE") + val columnMetadata = ColumnMetadata( + it.getString("COLUMN_NAME"), + type, + it.getBoolean("NULLABLE"), + it.getInt("COLUMN_SIZE").takeIf { it != 0 }, + autoIncrement, + // Not sure this filters enough but I dont think we ever want to have sequences here + defaultDbValue?.takeIf { !autoIncrement }, + ) + it.getString("TABLE_NAME") to columnMetadata + } + }) } return result @@ -308,6 +320,15 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) } } +/** + * Returns the set of schemas the tables are defined in + */ +internal fun extractSchemas(tables: Array): Set { + return tables.map { table -> + table.tableName.substringBefore(".") + }.toSet() +} + private fun ResultSet.iterate(body: ResultSet.() -> T): List { val result = arrayListOf() while (next()) { From 27fc1663eb8acdf2ce448df072f48978706cdceb Mon Sep 17 00:00:00 2001 From: Alexey Soshin Date: Wed, 1 Feb 2023 12:45:30 +0000 Subject: [PATCH 05/10] Rebase from main --- .../sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt index 14b9473e88..00be3b93a6 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt @@ -4,7 +4,6 @@ import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IdTable import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.LongIdTable -import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull import org.jetbrains.exposed.sql.tests.DatabaseTestsBase From 5ae746add19f3249a45e9c55992cb671de5ec5da Mon Sep 17 00:00:00 2001 From: Alexey Soshin Date: Wed, 1 Feb 2023 12:46:31 +0000 Subject: [PATCH 06/10] Use schema method to extract schemas --- .../exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt index 2c1d61bbe7..632cb6d09d 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt @@ -325,7 +325,7 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) */ internal fun extractSchemas(tables: Array): Set { return tables.map { table -> - table.tableName.substringBefore(".") + table.schema }.toSet() } From 8859614141e9cc36283c6ee630d3c228aecc34cc Mon Sep 17 00:00:00 2001 From: Alexey Soshin Date: Wed, 1 Feb 2023 15:25:39 +0000 Subject: [PATCH 07/10] Rebase from main --- .../statements/jdbc/JdbcDatabaseMetadataImpl.kt | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt index 632cb6d09d..e7d3114bd9 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt @@ -151,13 +151,6 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) override fun columns(vararg tables: Table): Map> { val databases = mutableListOf(databaseName) - // For MySQL, table names that contain schema are considered to be in their own DB - // For example, if you connect to testdb, but your table is my_schema.test_table, - // the metadata you need to query is for my_schema, not testdb - if (currentDialect is MysqlDialect) { - databases.addAll(extractSchemas(tables)) - } - val result = mutableMapOf>() for (database in databases) { val columnsMetadata = metadata.getColumns(database, null, "%", "%") @@ -320,15 +313,6 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) } } -/** - * Returns the set of schemas the tables are defined in - */ -internal fun extractSchemas(tables: Array): Set { - return tables.map { table -> - table.schema - }.toSet() -} - private fun ResultSet.iterate(body: ResultSet.() -> T): List { val result = arrayListOf() while (next()) { From 703ae25b40e3156160d428470f2534a71fec0c2c Mon Sep 17 00:00:00 2001 From: Chantal Loncle <82039410+bog-walk@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:15:01 -0400 Subject: [PATCH 08/10] Update fix to cover tables with index and foreign key columns --- .../org/jetbrains/exposed/sql/Constraints.kt | 10 +-- .../kotlin/org/jetbrains/exposed/sql/Table.kt | 13 ++-- .../jetbrains/exposed/sql/vendors/Default.kt | 6 +- .../jetbrains/exposed/sql/vendors/Mysql.kt | 10 +-- .../jdbc/JdbcDatabaseMetadataImpl.kt | 72 +++++++++++-------- .../ddl/CreateMissingTablesAndColumnsTests.kt | 32 +++++++++ 6 files changed, 96 insertions(+), 47 deletions(-) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt index c8f6d54a4d..9db3220733 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt @@ -257,17 +257,17 @@ data class Index( /** Name of the index. */ val indexName: String get() = customName ?: buildString { - append(table.nameInDatabaseCase()) + append(table.nameInDatabaseCaseUnquoted()) 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" } 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 7fd2c33a99..115ce24216 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 @@ -335,7 +335,10 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { else -> javaClass.name.removePrefix("${javaClass.`package`.name}.").substringAfter('$').removeSuffix("Table") } - internal val tableNameWithoutScheme: String get() = tableName.substringAfter(".") + /** Returns the schema name, or null if one does not exist for this table. */ + val schemaName: String? = if (name.contains(".")) name.substringBeforeLast(".") else null + + internal val tableNameWithoutScheme: String get() = tableName.substringAfterLast(".") // Table name may contain quotes, remove those before appending internal val tableNameWithoutSchemeSanitized: String get() = tableNameWithoutScheme.replace("\"", "").replace("'", "") @@ -369,15 +372,15 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { fun nameInDatabaseCase(): String = tableName.inProperCase() /** - * Returns the table name, in proper case, with wrapping single- and double-quotation characters removed. + * Returns the table name, without schema and in proper case, with wrapping single- and double-quotation characters removed. * - * **Note** If used with MySQL or MariaDB, the column name is returned unchanged, since these databases use a + * **Note** If used with MySQL or MariaDB, the table name is returned unchanged, since these databases use a * backtick character as the identifier quotation. */ fun nameInDatabaseCaseUnquoted(): String = if (currentDialect is MysqlDialect) { - nameInDatabaseCase() + tableNameWithoutScheme.inProperCase() } else { - nameInDatabaseCase().trim('\"', '\'') + tableNameWithoutScheme.inProperCase().trim('\"', '\'') } override fun describe(s: Transaction, queryBuilder: QueryBuilder): Unit = queryBuilder { append(s.identity(this@Table)) } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt index 5800624d12..fd7bb5f65c 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt @@ -1139,7 +1139,7 @@ abstract class VendorDialect( } override fun tableExists(table: Table): Boolean { - val tableScheme = table.tableName.substringBefore('.', "").takeIf { it.isNotEmpty() } + val tableScheme = table.schemaName val scheme = tableScheme?.inProperCase() ?: TransactionManager.current().connection.metadata { currentScheme } val allTables = getAllTableNamesCache().getValue(scheme) return allTables.any { @@ -1171,11 +1171,11 @@ abstract class VendorDialect( ): Map>>, List> { val constraints = HashMap>>, MutableList>() - val tablesToLoad = tables.filter { !columnConstraintsCache.containsKey(it.nameInDatabaseCase()) } + val tablesToLoad = tables.filter { !columnConstraintsCache.containsKey(it.nameInDatabaseCaseUnquoted()) } fillConstraintCacheForTables(tablesToLoad) tables.forEach { table -> - columnConstraintsCache[table.nameInDatabaseCase()].orEmpty().forEach { + columnConstraintsCache[table.nameInDatabaseCaseUnquoted()].orEmpty().forEach { constraints.getOrPut(table to it.from) { arrayListOf() }.add(it) } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt index 0077e545bb..8d9cd8af62 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt @@ -294,11 +294,11 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq } override fun fillConstraintCacheForTables(tables: List) { - val allTables = SchemaUtils.sortTablesByReferences(tables).associateBy { it.nameInDatabaseCase() } + val allTables = SchemaUtils.sortTablesByReferences(tables).associateBy { it.nameInDatabaseCaseUnquoted() } val allTableNames = allTables.keys val inTableList = allTableNames.joinToString("','", prefix = " ku.TABLE_NAME IN ('", postfix = "')") val tr = TransactionManager.current() - val schemaName = "'${getDatabase()}'" + val tableSchema = "'${tables.mapNotNull { it.schemaName }.toSet().singleOrNull() ?: getDatabase()}'" val constraintsToLoad = HashMap>() tr.exec( """SELECT @@ -312,9 +312,9 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku ON ku.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA AND rc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME - WHERE ku.TABLE_SCHEMA = $schemaName - AND ku.CONSTRAINT_SCHEMA = $schemaName - AND rc.CONSTRAINT_SCHEMA = $schemaName + WHERE ku.TABLE_SCHEMA = $tableSchema + AND ku.CONSTRAINT_SCHEMA = $tableSchema + AND rc.CONSTRAINT_SCHEMA = $tableSchema AND $inTableList ORDER BY ku.ORDINAL_POSITION """.trimIndent() diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt index e7d3114bd9..83c92dccf6 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt @@ -149,32 +149,32 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) * For each table, returns metadata for each of its columns */ override fun columns(vararg tables: Table): Map> { - val databases = mutableListOf(databaseName) - val result = mutableMapOf>() - for (database in databases) { - val columnsMetadata = metadata.getColumns(database, null, "%", "%") - result += (columnsMetadata.use { resultSet -> - resultSet.extractColumns(tables) { - // @see java.sql.DatabaseMetaData.getColumns - // That read should go first as Oracle driver closes connection after that - val defaultDbValue = it.getString("COLUMN_DEF")?.let { sanitizedDefault(it) } - val autoIncrement = it.getString("IS_AUTOINCREMENT") == "YES" - val type = it.getInt("DATA_TYPE") - val columnMetadata = ColumnMetadata( - it.getString("COLUMN_NAME"), - type, - it.getBoolean("NULLABLE"), - it.getInt("COLUMN_SIZE").takeIf { it != 0 }, - autoIncrement, - // Not sure this filters enough but I dont think we ever want to have sequences here - defaultDbValue?.takeIf { !autoIncrement }, - ) - it.getString("TABLE_NAME") to columnMetadata - } - }) + val useSchemaInsteadOfDatabase = currentDialect is MysqlDialect + + val tablesBySchema = tables.groupBy { identifierManager.inProperCase(it.schemaName ?: currentScheme) } + tablesBySchema.forEach { (schema, schemaTables) -> + val catalog = if (!useSchemaInsteadOfDatabase || schema == currentScheme) databaseName else schema + val rs = metadata.getColumns(catalog, schema, "%", "%") + result += rs.extractColumns(schemaTables.toTypedArray()) { + // @see java.sql.DatabaseMetaData.getColumns + // That read should go first as Oracle driver closes connection after that + val defaultDbValue = it.getString("COLUMN_DEF")?.let { sanitizedDefault(it) } + val autoIncrement = it.getString("IS_AUTOINCREMENT") == "YES" + val type = it.getInt("DATA_TYPE") + val columnMetadata = ColumnMetadata( + it.getString("COLUMN_NAME"), + type, + it.getBoolean("NULLABLE"), + it.getInt("COLUMN_SIZE").takeIf { it != 0 }, + autoIncrement, + // Not sure this filters enough but I dont think we ever want to have sequences here + defaultDbValue?.takeIf { !autoIncrement }, + ) + it.getString("TABLE_NAME") to columnMetadata + } + rs.close() } - return result } @@ -198,12 +198,14 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) private val existingIndicesCache = HashMap>() + @Suppress("CyclomaticComplexMethod") override fun existingIndices(vararg tables: Table): Map> { for (table in tables) { val transaction = TransactionManager.current() + val (catalog, tableSchema) = tableCatalogAndSchema(table) existingIndicesCache.getOrPut(table) { - val pkNames = metadata.getPrimaryKeys(databaseName, currentScheme, table.nameInDatabaseCaseUnquoted()).let { rs -> + val pkNames = metadata.getPrimaryKeys(catalog, tableSchema, table.nameInDatabaseCaseUnquoted()).let { rs -> val names = arrayListOf() while (rs.next()) { rs.getString("PK_NAME")?.let { names += it } @@ -211,7 +213,8 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) rs.close() names } - val rs = metadata.getIndexInfo(databaseName, currentScheme, table.nameInDatabaseCase(), false, false) + val storedIndexTable = if (tableSchema == currentScheme) table.nameInDatabaseCase() else table.nameInDatabaseCaseUnquoted() + val rs = metadata.getIndexInfo(catalog, tableSchema, storedIndexTable, false, false) val tmpIndices = hashMapOf, MutableList>() @@ -253,7 +256,8 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) override fun existingPrimaryKeys(vararg tables: Table): Map { return tables.associateWith { table -> - metadata.getPrimaryKeys(databaseName, currentScheme, table.nameInDatabaseCaseUnquoted()).let { rs -> + val (catalog, tableSchema) = tableCatalogAndSchema(table) + metadata.getPrimaryKeys(catalog, tableSchema, table.nameInDatabaseCaseUnquoted()).let { rs -> val columnNames = mutableListOf() var pkName = "" while (rs.next()) { @@ -268,9 +272,10 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) @Synchronized override fun tableConstraints(tables: List
): Map> { - val allTables = SchemaUtils.sortTablesByReferences(tables).associateBy { it.nameInDatabaseCase() } + val allTables = SchemaUtils.sortTablesByReferences(tables).associateBy { it.nameInDatabaseCaseUnquoted() } return allTables.keys.associateWith { table -> - metadata.getImportedKeys(databaseName, currentScheme, table).iterate { + val (catalog, tableSchema) = tableCatalogAndSchema(allTables[table]!!) + metadata.getImportedKeys(catalog, identifierManager.inProperCase(tableSchema), table).iterate { val fromTableName = getString("FKTABLE_NAME")!! val fromColumnName = identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(getString("FKCOLUMN_NAME")!!) val fromColumn = allTables[fromTableName]?.columns?.firstOrNull { @@ -301,6 +306,15 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) } } + private fun tableCatalogAndSchema(table: Table): Pair { + val tableSchema = identifierManager.inProperCase(table.schemaName ?: currentScheme) + return if (currentDialect is MysqlDialect && tableSchema != currentScheme) { + tableSchema to tableSchema + } else { + databaseName to tableSchema + } + } + @Synchronized override fun cleanCache() { existingIndicesCache.clear() diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt index 00be3b93a6..65db329aa9 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt @@ -651,4 +651,36 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { } } } + + @Test + fun testCreateTableWithSchemaPrefix() { + val schemaName = "my_schema" + val schema = Schema(schemaName) + // index and foreign key both use table name to auto-generate their own names & to compare metadata + val parentTable = object : IntIdTable("$schemaName.parent_table") { + val secondId = integer("second_id").uniqueIndex() + } + val childTable = object : LongIdTable("$schemaName.child_table") { + val parent = reference("my_parent", parentTable) + } + + // SQLite does not recognize creation of schema other than the attached database + withDb(excludeSettings = listOf(TestDB.SQLITE)) { testDb -> + SchemaUtils.createSchema(schema) + SchemaUtils.create(parentTable, childTable) + + try { + SchemaUtils.createMissingTablesAndColumns(parentTable, childTable) + assertTrue(parentTable.exists()) + assertTrue(childTable.exists()) + } finally { + if (testDb == TestDB.SQLSERVER) { + SchemaUtils.drop(childTable, parentTable) + SchemaUtils.dropSchema(schema) + } else { + SchemaUtils.dropSchema(schema, cascade = true) + } + } + } + } } From 3c35e63f0d378e7f7b46ad00b06149f8d42fb5b7 Mon Sep 17 00:00:00 2001 From: Chantal Loncle <82039410+bog-walk@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:31:02 -0400 Subject: [PATCH 09/10] Add apiDump --- exposed-core/api/exposed-core.api | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index ded68426f0..086c528da4 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -2240,6 +2240,7 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS public final fun getForeignKeys ()Ljava/util/List; public final fun getIndices ()Ljava/util/List; public fun getPrimaryKey ()Lorg/jetbrains/exposed/sql/Table$PrimaryKey; + public final fun getSchemaName ()Ljava/lang/String; public fun getTableName ()Ljava/lang/String; public fun hashCode ()I public final fun index (Ljava/lang/String;Z[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V @@ -3289,6 +3290,7 @@ public final class org/jetbrains/exposed/sql/vendors/DatabaseDialect$DefaultImpl public final class org/jetbrains/exposed/sql/vendors/DefaultKt { public static final fun getCurrentDialect ()Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect; + public static final fun inProperCase (Ljava/lang/String;)Ljava/lang/String; } public abstract class org/jetbrains/exposed/sql/vendors/ForUpdateOption { From 4921604a7c00d3ef50d6ecf3badaac055921e889 Mon Sep 17 00:00:00 2001 From: Chantal Loncle <82039410+bog-walk@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:50:18 -0400 Subject: [PATCH 10/10] Revert inProperCase visibility modifier change Add comment to private function. --- exposed-core/api/exposed-core.api | 1 - .../org/jetbrains/exposed/sql/vendors/Default.kt | 2 +- .../sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt | 12 +++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index 086c528da4..6567dd054b 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -3290,7 +3290,6 @@ public final class org/jetbrains/exposed/sql/vendors/DatabaseDialect$DefaultImpl public final class org/jetbrains/exposed/sql/vendors/DefaultKt { public static final fun getCurrentDialect ()Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect; - public static final fun inProperCase (Ljava/lang/String;)Ljava/lang/String; } public abstract class org/jetbrains/exposed/sql/vendors/ForUpdateOption { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt index fd7bb5f65c..d67bb8ed53 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt @@ -1328,5 +1328,5 @@ internal val currentDialectIfAvailable: DatabaseDialect? null } -fun String.inProperCase(): String = +internal fun String.inProperCase(): String = TransactionManager.currentOrNull()?.db?.identifierManager?.inProperCase(this@inProperCase) ?: this diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt index 83c92dccf6..13b69821c9 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt @@ -145,9 +145,6 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) return result } - /** - * For each table, returns metadata for each of its columns - */ override fun columns(vararg tables: Table): Map> { val result = mutableMapOf>() val useSchemaInsteadOfDatabase = currentDialect is MysqlDialect @@ -306,6 +303,15 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) } } + /** + * Returns the name of the database in which a [table] is found, as well as it's schema name. + * + * If the table name does not include a schema prefix, the metadata value `currentScheme` is used instead. + * + * MySQL/MariaDB are special cases in that a schema definition is treated like a separate database. This means that + * a connection to 'testDb' with a table defined as 'my_schema.my_table' will only successfully find the table's + * metadata if 'my_schema' is used as the database name. + */ private fun tableCatalogAndSchema(table: Table): Pair { val tableSchema = identifierManager.inProperCase(table.schemaName ?: currentScheme) return if (currentDialect is MysqlDialect && tableSchema != currentScheme) {