From a1e800848806a4011fe700c53c14edbc142cecc6 Mon Sep 17 00:00:00 2001 From: Alexey Zinoviev Date: Mon, 1 Dec 2025 19:20:48 +0100 Subject: [PATCH 1/5] Revert "Revert "Introduce `Mode` enum for H2 compatibility modes with deprecation of old constants"" This reverts commit 63ecf8a8ef79386e1a60381099c43e366fa15ce2. --- .../jetbrains/kotlinx/dataframe/io/db/H2.kt | 35 +++++++++++++++---- .../kotlinx/dataframe/io/h2/h2Test.kt | 25 +++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt index 4a7b47481d..aa5650c71c 100644 --- a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt +++ b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt @@ -18,6 +18,7 @@ public open class H2(public val dialect: DbType = MySql) : DbType("h2") { require(dialect::class != H2::class) { "H2 database could not be specified with H2 dialect!" } } + /** * It contains constants related to different database modes. * @@ -29,16 +30,36 @@ public open class H2(public val dialect: DbType = MySql) : DbType("h2") { * @see [createH2Instance] */ public companion object { - /** It represents the mode value "MySQL" for the H2 database. */ - public const val MODE_MYSQL: String = "MySQL" + /** + * Represents the compatibility modes supported by an H2 database. + * + * @property value The string value used in H2 JDBC URL and settings. + */ + public enum class Mode(public val value: String) { + MySql("MySQL"), + PostgreSql("PostgreSQL"), + MsSqlServer("MSSQLServer"), + MariaDb("MariaDB"); - /** It represents the mode value "PostgreSQL" for the H2 database. */ - public const val MODE_POSTGRESQL: String = "PostgreSQL" + public companion object { + /** + * Finds a Mode by its string value (case-insensitive). + * + * @param value The string value to search for. + * @return The matching Mode, or null if not found. + */ + public fun fromValue(value: String): Mode? = + entries.find { it.value.equals(value, ignoreCase = true) } + } + } - /** It represents the mode value "MSSQLServer" for the H2 database. */ + @Deprecated("Use Mode.MySql.value instead", ReplaceWith("Mode.MySql.value")) + public const val MODE_MYSQL: String = "MySQL" + @Deprecated("Use Mode.PostgreSql.value instead", ReplaceWith("Mode.PostgreSql.value")) + public const val MODE_POSTGRESQL: String = "PostgreSQL" + @Deprecated("Use Mode.MsSqlServer.value instead", ReplaceWith("Mode.MsSqlServer.value")) public const val MODE_MSSQLSERVER: String = "MSSQLServer" - - /** It represents the mode value "MariaDB" for the H2 database. */ + @Deprecated("Use Mode.MariaDb.value instead", ReplaceWith("Mode.MariaDb.value")) public const val MODE_MARIADB: String = "MariaDB" } diff --git a/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt b/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt index 9d0d0c8279..3415b55568 100644 --- a/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt +++ b/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt @@ -974,6 +974,31 @@ class JdbcTest { exception.message shouldBe "H2 database could not be specified with H2 dialect!" } + + @Test + fun `regular mode for H2 with DbConnectionConfig`() { + val url = "jdbc:h2:mem:testDatabase" + val username = "sa" + val password = "" + + val dbConfig = DbConnectionConfig(url, username, password) + + val df = DataFrame.readSqlQuery(dbConfig, "SELECT 1") + df.rowsCount() shouldBe 1 + } + + @Test + fun `regular mode for H2 with Connection`() { + val url = "jdbc:h2:mem:testDatabase" + val username = "sa" + val password = "" + + DriverManager.getConnection(url, username, password).use { connection -> + val df = DataFrame.readSqlQuery(connection, "SELECT 1") + df.rowsCount() shouldBe 1 + } + } + // helper object created for API testing purposes object CustomDB : H2(MySql) From 78c1073910fb033834b6f9d2264305d1c28e9983 Mon Sep 17 00:00:00 2001 From: Alexey Zinoviev Date: Mon, 1 Dec 2025 19:20:54 +0100 Subject: [PATCH 2/5] Revert "Revert "Add enhanced support for H2 compatibility modes via `Mode` enumeration and refine database handling"" This reverts commit 003707065f122a4e2df354e6257189d31ad74a48. --- .../jetbrains/kotlinx/dataframe/io/db/H2.kt | 131 +++++++++++---- .../jetbrains/kotlinx/dataframe/io/db/util.kt | 152 ++++++++---------- .../kotlinx/dataframe/io/h2/h2Test.kt | 117 +++++++++++--- .../StardustDocs/topics/dataSources/sql/H2.md | 27 +++- 4 files changed, 294 insertions(+), 133 deletions(-) diff --git a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt index aa5650c71c..d81b3d223a 100644 --- a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt +++ b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt @@ -1,6 +1,10 @@ package org.jetbrains.kotlinx.dataframe.io.db import org.jetbrains.kotlinx.dataframe.schema.ColumnSchema +import org.jetbrains.kotlinx.dataframe.io.db.MariaDb as MariaDbType +import org.jetbrains.kotlinx.dataframe.io.db.MsSql as MsSqlType +import org.jetbrains.kotlinx.dataframe.io.db.MySql as MySqlType +import org.jetbrains.kotlinx.dataframe.io.db.PostgreSql as PostgreSqlType import java.sql.ResultSet import java.util.Locale import kotlin.reflect.KType @@ -13,12 +17,89 @@ import kotlin.reflect.KType * * NOTE: All date and timestamp-related types are converted to String to avoid java.sql.* types. */ -public open class H2(public val dialect: DbType = MySql) : DbType("h2") { - init { - require(dialect::class != H2::class) { "H2 database could not be specified with H2 dialect!" } - } +public open class H2(public val mode: Mode = Mode.Regular) : DbType("h2") { + @Deprecated("Use H2(mode = Mode.XXX) instead", ReplaceWith("H2(H2.Mode.MySql)")) + public constructor(dialect: DbType) : this( + Mode.fromDbType(dialect) + ?: throw IllegalArgumentException("H2 database could not be specified with H2 dialect!"), + ) + + private val delegate: DbType? = mode.toDbType() + + /** + * Represents the compatibility modes supported by an H2 database. + * + * @property value The string value used in H2 JDBC URL and settings. + */ + public enum class Mode(public val value: String) { + /** Native H2 mode (no compatibility), our synthetic marker. */ + Regular("H2-Regular"), + MySql("MySQL"), + PostgreSql("PostgreSQL"), + MsSqlServer("MSSQLServer"), + MariaDb("MariaDB"); + + /** + * Converts this Mode to the corresponding DbType delegate. + * + * @return The DbType for this mode, or null for Regular mode. + */ + public fun toDbType(): DbType? = when (this) { + Regular -> null + MySql -> MySqlType + PostgreSql -> PostgreSqlType + MsSqlServer -> MsSqlType + MariaDb -> MariaDbType + } + + public companion object { + /** + * Creates a Mode from the given DbType. + * + * @param dialect The DbType to convert. + * @return The corresponding Mode, or null if the dialect is H2. + */ + public fun fromDbType(dialect: DbType): Mode? = when (dialect) { + is H2 -> null + MySqlType -> MySql + PostgreSqlType -> PostgreSql + MsSqlType -> MsSqlServer + MariaDbType -> MariaDb + else -> Regular + } + + /** + * Finds a Mode by its string value (case-insensitive). + * Handles both URL values (MySQL, PostgreSQL, etc.) and + * INFORMATION_SCHEMA values (Regular). + * + * @param value The string value to search for. + * @return The matching Mode, or null if not found. + */ + public fun fromValue(value: String): Mode? { + // "Regular" from INFORMATION_SCHEMA or "H2-Regular" from URL + if (value.equals("regular", ignoreCase = true) || + value.equals("h2-regular", ignoreCase = true)) { + return Regular + } + return entries.find { it.value.equals(value, ignoreCase = true) } + } + /** + * Parses a string that may be an H2 MODE value into a Mode. + * Accepts case-insensitive `regular` and `h2-regular` as Regular. + * + * @param mode The mode string to parse, or null for Regular mode. + * @return The corresponding Mode for null/empty input or supported modes. + * @throws IllegalArgumentException if the mode is not null and not supported. + */ + public fun fromString(mode: String?): Mode? { + if (mode == null) return null + return fromValue(mode) + } + } + } /** * It contains constants related to different database modes. * @@ -30,28 +111,6 @@ public open class H2(public val dialect: DbType = MySql) : DbType("h2") { * @see [createH2Instance] */ public companion object { - /** - * Represents the compatibility modes supported by an H2 database. - * - * @property value The string value used in H2 JDBC URL and settings. - */ - public enum class Mode(public val value: String) { - MySql("MySQL"), - PostgreSql("PostgreSQL"), - MsSqlServer("MSSQLServer"), - MariaDb("MariaDB"); - - public companion object { - /** - * Finds a Mode by its string value (case-insensitive). - * - * @param value The string value to search for. - * @return The matching Mode, or null if not found. - */ - public fun fromValue(value: String): Mode? = - entries.find { it.value.equals(value, ignoreCase = true) } - } - } @Deprecated("Use Mode.MySql.value instead", ReplaceWith("Mode.MySql.value")) public const val MODE_MYSQL: String = "MySQL" @@ -67,7 +126,7 @@ public open class H2(public val dialect: DbType = MySql) : DbType("h2") { get() = "org.h2.Driver" override fun convertSqlTypeToColumnSchemaValue(tableColumnMetadata: TableColumnMetadata): ColumnSchema? = - dialect.convertSqlTypeToColumnSchemaValue(tableColumnMetadata) + delegate?.convertSqlTypeToColumnSchemaValue(tableColumnMetadata) override fun isSystemTable(tableMetadata: TableMetadata): Boolean { val locale = Locale.getDefault() @@ -78,14 +137,24 @@ public open class H2(public val dialect: DbType = MySql) : DbType("h2") { // could be extended for other symptoms of the system tables for H2 val isH2SystemTable = schemaName.containsWithLowercase("information_schema") - return isH2SystemTable || dialect.isSystemTable(tableMetadata) + return if (delegate == null) { + isH2SystemTable + } else { + isH2SystemTable || delegate.isSystemTable(tableMetadata) + } } - override fun buildTableMetadata(tables: ResultSet): TableMetadata = dialect.buildTableMetadata(tables) + override fun buildTableMetadata(tables: ResultSet): TableMetadata = + delegate?.buildTableMetadata(tables) + ?: TableMetadata( + tables.getString("table_name"), + tables.getString("table_schem"), + tables.getString("table_cat"), + ) override fun convertSqlTypeToKType(tableColumnMetadata: TableColumnMetadata): KType? = - dialect.convertSqlTypeToKType(tableColumnMetadata) + delegate?.convertSqlTypeToKType(tableColumnMetadata) public override fun buildSqlQueryWithLimit(sqlQuery: String, limit: Int): String = - dialect.buildSqlQueryWithLimit(sqlQuery, limit) + delegate?.buildSqlQueryWithLimit(sqlQuery, limit) ?: super.buildSqlQueryWithLimit(sqlQuery, limit) } diff --git a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt index 1d6752cf90..91f46ad3a6 100644 --- a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt +++ b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt @@ -7,8 +7,17 @@ import java.util.Locale private val logger = KotlinLogging.logger {} +private const val UNSUPPORTED_H2_MODE_MESSAGE = + "Unsupported H2 MODE: %s. Supported: MySQL, PostgreSQL, MSSQLServer, MariaDB, REGULAR/H2-Regular (or omit MODE)." + +private const val H2_MODE_QUERY = "SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE'" + +private val H2_MODE_URL_PATTERN = "MODE=([^;:&]+)".toRegex(RegexOption.IGNORE_CASE) + /** * Extracts the database type from the given connection. + * For H2, fetches the actual MODE from the active connection settings. + * For other databases, extracts type from URL. * * @param [connection] the database connection. * @return the corresponding [DbType]. @@ -21,44 +30,56 @@ public fun extractDBTypeFromConnection(connection: Connection): DbType { ?: throw IllegalStateException("URL information is missing in connection meta data!") logger.info { "Processing DB type extraction for connection url: $url" } - return if (url.contains(H2().dbTypeInJdbcUrl)) { - // works only for H2 version 2 - val modeQuery = "SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE'" - var mode = "" - connection.prepareStatement(modeQuery).use { st -> - st.executeQuery().use { rs -> - if (rs.next()) { - mode = rs.getString("SETTING_VALUE") - logger.debug { "Fetched H2 DB mode: $mode" } - } else { - throw IllegalStateException("The information about H2 mode is not found in the H2 meta-data!") - } - } - } - - // H2 doesn't support MariaDB and SQLite - when (mode.lowercase(Locale.getDefault())) { - H2.MODE_MYSQL.lowercase(Locale.getDefault()) -> H2(MySql) + // First, determine the base database type from URL + val baseDbType = extractDBTypeFromUrl(url) - H2.MODE_MSSQLSERVER.lowercase(Locale.getDefault()) -> H2(MsSql) - - H2.MODE_POSTGRESQL.lowercase(Locale.getDefault()) -> H2(PostgreSql) - - H2.MODE_MARIADB.lowercase(Locale.getDefault()) -> H2(MariaDb) - - else -> { - val message = "Unsupported database type in the url: $url. " + - "Only MySQL, MariaDB, MSSQL and PostgreSQL are supported!" - logger.error { message } + // For H2, refine the mode by querying the active connection settings + // This handles cases where MODE is not specified in URL, but H2 returns "Regular" from settings + return if (baseDbType is H2) { + val mode = fetchH2ModeFromConnection(connection) + parseH2ModeOrThrow(mode) + } else { + logger.info { "Identified DB type as $baseDbType from url: $url" } + baseDbType + } +} - throw IllegalArgumentException(message) +/** + * Fetches H2 database mode from an active connection. + * Works only for H2 version 2. + * + * @param [connection] the database connection. + * @return the mode string or null if not set. + */ +private fun fetchH2ModeFromConnection(connection: Connection): String? { + var mode: String? = null + connection.prepareStatement(H2_MODE_QUERY).use { st -> + st.executeQuery().use { rs -> + if (rs.next()) { + mode = rs.getString("SETTING_VALUE") + logger.debug { "Fetched H2 DB mode: $mode" } } } - } else { - val dbType = extractDBTypeFromUrl(url) - logger.info { "Identified DB type as $dbType from url: $url" } - dbType } + + return mode?.trim()?.takeIf { it.isNotEmpty() } +} + +/** + * Parses H2 mode string and returns the corresponding H2 DbType instance. + * + * @param [mode] the mode string (maybe null or empty for Regular mode). + * @return H2 instance with the appropriate mode. + * @throws [IllegalArgumentException] if the mode is not supported. + */ +private fun parseH2ModeOrThrow(mode: String?): H2 { + if (mode.isNullOrEmpty()) { + return H2(H2.Mode.Regular) + } + return H2.Mode.fromValue(mode)?.let { H2(it) } + ?: throw IllegalArgumentException(UNSUPPORTED_H2_MODE_MESSAGE.format(mode)).also { + logger.error { it.message } + } } /** @@ -66,33 +87,24 @@ public fun extractDBTypeFromConnection(connection: Connection): DbType { * * @param [url] the JDBC URL. * @return the corresponding [DbType]. - * @throws [RuntimeException] if the url is null. + * @throws [SQLException] if the url is null. + * @throws [IllegalArgumentException] if the URL specifies an unsupported database type. */ public fun extractDBTypeFromUrl(url: String?): DbType { - if (url != null) { - val helperH2Instance = H2() - return when { - helperH2Instance.dbTypeInJdbcUrl in url -> createH2Instance(url) - - MariaDb.dbTypeInJdbcUrl in url -> MariaDb - - MySql.dbTypeInJdbcUrl in url -> MySql - - Sqlite.dbTypeInJdbcUrl in url -> Sqlite - - PostgreSql.dbTypeInJdbcUrl in url -> PostgreSql - - MsSql.dbTypeInJdbcUrl in url -> MsSql - - DuckDb.dbTypeInJdbcUrl in url -> DuckDb - - else -> throw IllegalArgumentException( - "Unsupported database type in the url: $url. " + + url ?: throw SQLException("Database URL could not be null.") + + return when { + H2().dbTypeInJdbcUrl in url -> createH2Instance(url) + MariaDb.dbTypeInJdbcUrl in url -> MariaDb + MySql.dbTypeInJdbcUrl in url -> MySql + Sqlite.dbTypeInJdbcUrl in url -> Sqlite + PostgreSql.dbTypeInJdbcUrl in url -> PostgreSql + MsSql.dbTypeInJdbcUrl in url -> MsSql + DuckDb.dbTypeInJdbcUrl in url -> DuckDb + else -> throw IllegalArgumentException( + "Unsupported database type in the url: $url. " + "Only H2, MariaDB, MySQL, MSSQL, SQLite, PostgreSQL, and DuckDB are supported!", - ) - } - } else { - throw SQLException("Database URL could not be null. The existing value is $url") + ) } } @@ -104,30 +116,8 @@ public fun extractDBTypeFromUrl(url: String?): DbType { * @throws [IllegalArgumentException] if the provided URL does not contain a valid mode. */ private fun createH2Instance(url: String): DbType { - val modePattern = "MODE=(.*?);".toRegex() - val matchResult = modePattern.find(url) - - val mode: String = if (matchResult != null && matchResult.groupValues.size == 2) { - matchResult.groupValues[1] - } else { - throw IllegalArgumentException("The provided URL `$url` does not contain a valid mode.") - } - - // H2 doesn't support MariaDB and SQLite - return when (mode.lowercase(Locale.getDefault())) { - H2.MODE_MYSQL.lowercase(Locale.getDefault()) -> H2(MySql) - - H2.MODE_MSSQLSERVER.lowercase(Locale.getDefault()) -> H2(MsSql) - - H2.MODE_POSTGRESQL.lowercase(Locale.getDefault()) -> H2(PostgreSql) - - H2.MODE_MARIADB.lowercase(Locale.getDefault()) -> H2(MariaDb) - - else -> throw IllegalArgumentException( - "Unsupported database mode: $mode. " + - "Only MySQL, MariaDB, MSSQL, PostgreSQL modes are supported!", - ) - } + val mode = H2_MODE_URL_PATTERN.find(url)?.groupValues?.getOrNull(1) + return parseH2ModeOrThrow(mode?.takeIf { it.isNotBlank() }) } /** diff --git a/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt b/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt index 3415b55568..d937a9143d 100644 --- a/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt +++ b/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt @@ -15,7 +15,6 @@ import org.jetbrains.kotlinx.dataframe.api.filter import org.jetbrains.kotlinx.dataframe.api.select import org.jetbrains.kotlinx.dataframe.io.DbConnectionConfig import org.jetbrains.kotlinx.dataframe.io.db.H2 -import org.jetbrains.kotlinx.dataframe.io.db.MySql import org.jetbrains.kotlinx.dataframe.io.inferNullability import org.jetbrains.kotlinx.dataframe.io.readAllSqlTables import org.jetbrains.kotlinx.dataframe.io.readDataFrame @@ -33,12 +32,15 @@ import java.sql.Connection import java.sql.DriverManager import java.sql.ResultSet import java.sql.SQLException +import org.jetbrains.kotlinx.dataframe.io.db.H2.Mode import kotlin.reflect.typeOf private const val URL = "jdbc:h2:mem:test5;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_UPPER=false" private const val MAXIMUM_POOL_SIZE = 5 +private const val QUERY_SELECT_ONE = "SELECT 1" + @DataSchema interface Customer { val id: Int? @@ -478,22 +480,24 @@ class JdbcTest { @Test fun `read from ResultSet`() { + val dbType = H2(Mode.MySql) + connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE).use { st -> @Language("SQL") val selectStatement = "SELECT * FROM Customer" st.executeQuery(selectStatement).use { rs -> - val df = DataFrame.readResultSet(rs, H2(MySql)) + val df = DataFrame.readResultSet(rs, dbType) assertCustomerData(df) rs.beforeFirst() - val df1 = DataFrame.readResultSet(rs, H2(MySql), 1) + val df1 = DataFrame.readResultSet(rs, dbType, 1) assertCustomerData(df1, 1) rs.beforeFirst() - val dataSchema = DataFrameSchema.readResultSet(rs, H2(MySql)) + val dataSchema = DataFrameSchema.readResultSet(rs, dbType) assertCustomerSchema(dataSchema) rs.beforeFirst() @@ -508,7 +512,7 @@ class JdbcTest { rs.beforeFirst() - val dataSchema1 = DataFrameSchema.readResultSet(rs, H2(MySql)) + val dataSchema1 = DataFrameSchema.readResultSet(rs, dbType) assertCustomerSchema(dataSchema1) } } @@ -516,22 +520,24 @@ class JdbcTest { @Test fun `read from extension function on ResultSet`() { + val dbType = H2(Mode.MySql) + connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE).use { st -> @Language("SQL") val selectStatement = "SELECT * FROM Customer" st.executeQuery(selectStatement).use { rs -> - val df = rs.readDataFrame(H2(MySql)) + val df = rs.readDataFrame(dbType) assertCustomerData(df) rs.beforeFirst() - val df1 = rs.readDataFrame(H2(MySql), 1) + val df1 = rs.readDataFrame(dbType, 1) assertCustomerData(df1, 1) rs.beforeFirst() - val dataSchema = rs.readDataFrameSchema(H2(MySql)) + val dataSchema = rs.readDataFrameSchema(dbType) assertCustomerSchema(dataSchema) rs.beforeFirst() @@ -546,7 +552,7 @@ class JdbcTest { rs.beforeFirst() - val dataSchema1 = rs.readDataFrameSchema(H2(MySql)) + val dataSchema1 = rs.readDataFrameSchema(dbType) assertCustomerSchema(dataSchema1) } } @@ -563,7 +569,7 @@ class JdbcTest { repeat(10) { rs.beforeFirst() - val df1 = DataFrame.readResultSet(rs, H2(MySql), 2) + val df1 = DataFrame.readResultSet(rs, H2(Mode.MySql), 2) assertCustomerData(df1, 2) rs.beforeFirst() @@ -974,33 +980,104 @@ class JdbcTest { exception.message shouldBe "H2 database could not be specified with H2 dialect!" } - @Test fun `regular mode for H2 with DbConnectionConfig`() { val url = "jdbc:h2:mem:testDatabase" - val username = "sa" - val password = "" - val dbConfig = DbConnectionConfig(url, username, password) + val dbConfig = DbConnectionConfig(url) - val df = DataFrame.readSqlQuery(dbConfig, "SELECT 1") + val df = DataFrame.readSqlQuery(dbConfig, QUERY_SELECT_ONE) df.rowsCount() shouldBe 1 } @Test fun `regular mode for H2 with Connection`() { val url = "jdbc:h2:mem:testDatabase" - val username = "sa" - val password = "" - DriverManager.getConnection(url, username, password).use { connection -> - val df = DataFrame.readSqlQuery(connection, "SELECT 1") + DriverManager.getConnection(url).use { connection -> + val df = DataFrame.readSqlQuery(connection, QUERY_SELECT_ONE) df.rowsCount() shouldBe 1 } } + // ========== H2 Mode Tests ========== + + private fun testH2ModeWithDbConnectionConfig(modeUrl: String) { + val dbConfig = DbConnectionConfig(modeUrl) + val df = DataFrame.readSqlQuery(dbConfig, QUERY_SELECT_ONE) + df.rowsCount() shouldBe 1 + } + + private fun testH2ModeWithConnection(modeUrl: String) { + DriverManager.getConnection(modeUrl).use { connection -> + val df = DataFrame.readSqlQuery(connection, QUERY_SELECT_ONE) + df.rowsCount() shouldBe 1 + } + } + + @Test + fun `MySQL mode for H2 with DbConnectionConfig`() { + testH2ModeWithDbConnectionConfig("jdbc:h2:mem:testMySql;MODE=MySQL") + } + + @Test + fun `MySQL mode for H2 with Connection`() { + testH2ModeWithConnection("jdbc:h2:mem:testMySql;MODE=MySQL") + } + + @Test + fun `PostgreSQL mode for H2 with DbConnectionConfig`() { + testH2ModeWithDbConnectionConfig("jdbc:h2:mem:testPostgres;MODE=PostgreSQL") + } + + @Test + fun `PostgreSQL mode for H2 with Connection`() { + testH2ModeWithConnection("jdbc:h2:mem:testPostgres;MODE=PostgreSQL") + } + + @Test + fun `MSSQLServer mode for H2 with DbConnectionConfig`() { + testH2ModeWithDbConnectionConfig("jdbc:h2:mem:testMsSql;MODE=MSSQLServer") + } + + @Test + fun `MSSQLServer mode for H2 with Connection`() { + testH2ModeWithConnection("jdbc:h2:mem:testMsSql;MODE=MSSQLServer") + } + + @Test + fun `MariaDB mode for H2 with DbConnectionConfig`() { + testH2ModeWithDbConnectionConfig("jdbc:h2:mem:testMariaDb;MODE=MariaDB") + } + + @Test + fun `MariaDB mode for H2 with Connection`() { + testH2ModeWithConnection("jdbc:h2:mem:testMariaDb;MODE=MariaDB") + } + + @Test + fun `H2 with unsupported mode throws exception`() { + val url = "jdbc:h2:mem:testUnsupported;MODE=DB2" + + DriverManager.getConnection(url).use { connection -> + shouldThrow { + DataFrame.readSqlQuery(connection, QUERY_SELECT_ONE) + } + } + } + + @Test + fun `H2 with unsupported mode throws exception using DbConnectionConfig`() { + val url = "jdbc:h2:mem:testUnsupported;MODE=Oracle" + val dbConfig = DbConnectionConfig(url) + + shouldThrow { + DataFrame.readSqlQuery(dbConfig, QUERY_SELECT_ONE) + } + } + // helper object created for API testing purposes - object CustomDB : H2(MySql) + object CustomDB : H2(Mode.MySql) @Test fun `read from table from custom database`() { diff --git a/docs/StardustDocs/topics/dataSources/sql/H2.md b/docs/StardustDocs/topics/dataSources/sql/H2.md index a288628fd7..4f0f228c2d 100644 --- a/docs/StardustDocs/topics/dataSources/sql/H2.md +++ b/docs/StardustDocs/topics/dataSources/sql/H2.md @@ -56,18 +56,43 @@ It is also possible to load all data from non-system tables, each into a separat See [](readSqlDatabases.md) for more details. +### H2 Compatibility Modes + +When working with H2 database, the library automatically detects the compatibility mode from the connection. +If no `MODE` is specified in the JDBC URL, the default `Regular` mode is used. +H2 supports the following compatibility modes: `MySQL`, `PostgreSQL`, `MSSQLServer`, `MariaDB`, and `Regular`. + ```kotlin import org.jetbrains.kotlinx.dataframe.io.DbConnectionConfig import org.jetbrains.kotlinx.dataframe.api.* +// Basic H2 connection (uses Regular mode by default) + val url = "jdbc:h2:mem:testDatabase" val username = "sa" val password = "" -val dbConfig = DbConnectionConfig(url, username, password) +val dbConfig = DbConnectionConfig(url, "sa", "") val tableName = "Customer" val df = DataFrame.readSqlTable(dbConfig, tableName) ``` +```kotlin +import org.jetbrains.kotlinx.dataframe.io.DbConnectionConfig +import org.jetbrains.kotlinx.dataframe.api.* + +// H2 with PostgreSQL compatibility mode + +val postgresUrl = "jdbc:h2:mem:testDatabase;MODE=PostgreSQL" +val username = "sa" +val password = "" + +val postgresConfig = DbConnectionConfig(postgresUrl, username, password) + +val tableName = "Customer" + +val dfPostgres = DataFrame.readSqlTable(postgresConfig, tableName) +``` + From 5b68f9090169cb8aa1428c5e0cca4030246a9b79 Mon Sep 17 00:00:00 2001 From: Alexey Zinoviev Date: Mon, 1 Dec 2025 19:28:33 +0100 Subject: [PATCH 3/5] Refine `Mode` enum handling in H2 implementation Polished the `H2` compatibility `Mode` enum with improved formatting and readability. Addressed minor import reorganizations, streamlined `toDbType` and `fromDbType` methods, and fixed consistency in error messages. Updated tests and utilities accordingly. --- .../jetbrains/kotlinx/dataframe/io/db/H2.kt | 47 +++++++++++-------- .../jetbrains/kotlinx/dataframe/io/db/util.kt | 10 +++- .../kotlinx/dataframe/io/h2/h2Test.kt | 2 +- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt index d81b3d223a..aaa50226a5 100644 --- a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt +++ b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt @@ -1,13 +1,13 @@ package org.jetbrains.kotlinx.dataframe.io.db import org.jetbrains.kotlinx.dataframe.schema.ColumnSchema +import java.sql.ResultSet +import java.util.Locale +import kotlin.reflect.KType import org.jetbrains.kotlinx.dataframe.io.db.MariaDb as MariaDbType import org.jetbrains.kotlinx.dataframe.io.db.MsSql as MsSqlType import org.jetbrains.kotlinx.dataframe.io.db.MySql as MySqlType import org.jetbrains.kotlinx.dataframe.io.db.PostgreSql as PostgreSqlType -import java.sql.ResultSet -import java.util.Locale -import kotlin.reflect.KType /** * Represents the H2 database type. @@ -38,20 +38,21 @@ public open class H2(public val mode: Mode = Mode.Regular) : DbType("h2") { MySql("MySQL"), PostgreSql("PostgreSQL"), MsSqlServer("MSSQLServer"), - MariaDb("MariaDB"); + MariaDb("MariaDB"), ; /** * Converts this Mode to the corresponding DbType delegate. * * @return The DbType for this mode, or null for Regular mode. */ - public fun toDbType(): DbType? = when (this) { - Regular -> null - MySql -> MySqlType - PostgreSql -> PostgreSqlType - MsSqlServer -> MsSqlType - MariaDb -> MariaDbType - } + public fun toDbType(): DbType? = + when (this) { + Regular -> null + MySql -> MySqlType + PostgreSql -> PostgreSqlType + MsSqlServer -> MsSqlType + MariaDb -> MariaDbType + } public companion object { /** @@ -60,14 +61,15 @@ public open class H2(public val mode: Mode = Mode.Regular) : DbType("h2") { * @param dialect The DbType to convert. * @return The corresponding Mode, or null if the dialect is H2. */ - public fun fromDbType(dialect: DbType): Mode? = when (dialect) { - is H2 -> null - MySqlType -> MySql - PostgreSqlType -> PostgreSql - MsSqlType -> MsSqlServer - MariaDbType -> MariaDb - else -> Regular - } + public fun fromDbType(dialect: DbType): Mode? = + when (dialect) { + is H2 -> null + MySqlType -> MySql + PostgreSqlType -> PostgreSql + MsSqlType -> MsSqlServer + MariaDbType -> MariaDb + else -> Regular + } /** * Finds a Mode by its string value (case-insensitive). @@ -80,7 +82,8 @@ public open class H2(public val mode: Mode = Mode.Regular) : DbType("h2") { public fun fromValue(value: String): Mode? { // "Regular" from INFORMATION_SCHEMA or "H2-Regular" from URL if (value.equals("regular", ignoreCase = true) || - value.equals("h2-regular", ignoreCase = true)) { + value.equals("h2-regular", ignoreCase = true) + ) { return Regular } return entries.find { it.value.equals(value, ignoreCase = true) } @@ -100,6 +103,7 @@ public open class H2(public val mode: Mode = Mode.Regular) : DbType("h2") { } } } + /** * It contains constants related to different database modes. * @@ -114,10 +118,13 @@ public open class H2(public val mode: Mode = Mode.Regular) : DbType("h2") { @Deprecated("Use Mode.MySql.value instead", ReplaceWith("Mode.MySql.value")) public const val MODE_MYSQL: String = "MySQL" + @Deprecated("Use Mode.PostgreSql.value instead", ReplaceWith("Mode.PostgreSql.value")) public const val MODE_POSTGRESQL: String = "PostgreSQL" + @Deprecated("Use Mode.MsSqlServer.value instead", ReplaceWith("Mode.MsSqlServer.value")) public const val MODE_MSSQLSERVER: String = "MSSQLServer" + @Deprecated("Use Mode.MariaDb.value instead", ReplaceWith("Mode.MariaDb.value")) public const val MODE_MARIADB: String = "MariaDB" } diff --git a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt index 91f46ad3a6..a0a83d6fa0 100644 --- a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt +++ b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt @@ -3,7 +3,6 @@ package org.jetbrains.kotlinx.dataframe.io.db import io.github.oshai.kotlinlogging.KotlinLogging import java.sql.Connection import java.sql.SQLException -import java.util.Locale private val logger = KotlinLogging.logger {} @@ -95,15 +94,22 @@ public fun extractDBTypeFromUrl(url: String?): DbType { return when { H2().dbTypeInJdbcUrl in url -> createH2Instance(url) + MariaDb.dbTypeInJdbcUrl in url -> MariaDb + MySql.dbTypeInJdbcUrl in url -> MySql + Sqlite.dbTypeInJdbcUrl in url -> Sqlite + PostgreSql.dbTypeInJdbcUrl in url -> PostgreSql + MsSql.dbTypeInJdbcUrl in url -> MsSql + DuckDb.dbTypeInJdbcUrl in url -> DuckDb + else -> throw IllegalArgumentException( "Unsupported database type in the url: $url. " + - "Only H2, MariaDB, MySQL, MSSQL, SQLite, PostgreSQL, and DuckDB are supported!", + "Only H2, MariaDB, MySQL, MSSQL, SQLite, PostgreSQL, and DuckDB are supported!", ) } } diff --git a/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt b/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt index d937a9143d..4a35ad4eb1 100644 --- a/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt +++ b/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt @@ -15,6 +15,7 @@ import org.jetbrains.kotlinx.dataframe.api.filter import org.jetbrains.kotlinx.dataframe.api.select import org.jetbrains.kotlinx.dataframe.io.DbConnectionConfig import org.jetbrains.kotlinx.dataframe.io.db.H2 +import org.jetbrains.kotlinx.dataframe.io.db.H2.Mode import org.jetbrains.kotlinx.dataframe.io.inferNullability import org.jetbrains.kotlinx.dataframe.io.readAllSqlTables import org.jetbrains.kotlinx.dataframe.io.readDataFrame @@ -32,7 +33,6 @@ import java.sql.Connection import java.sql.DriverManager import java.sql.ResultSet import java.sql.SQLException -import org.jetbrains.kotlinx.dataframe.io.db.H2.Mode import kotlin.reflect.typeOf private const val URL = "jdbc:h2:mem:test5;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_UPPER=false" From 36db59b9d6e46116e75648e8026be166a8c04b3f Mon Sep 17 00:00:00 2001 From: zaleslaw Date: Tue, 2 Dec 2025 08:56:44 +0100 Subject: [PATCH 4/5] Api Dump --- dataframe-jdbc/api/dataframe-jdbc.api | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/dataframe-jdbc/api/dataframe-jdbc.api b/dataframe-jdbc/api/dataframe-jdbc.api index bd57ed2bcd..24ecebd50e 100644 --- a/dataframe-jdbc/api/dataframe-jdbc.api +++ b/dataframe-jdbc/api/dataframe-jdbc.api @@ -135,19 +135,40 @@ public class org/jetbrains/kotlinx/dataframe/io/db/H2 : org/jetbrains/kotlinx/da public static final field MODE_POSTGRESQL Ljava/lang/String; public fun ()V public fun (Lorg/jetbrains/kotlinx/dataframe/io/db/DbType;)V - public synthetic fun (Lorg/jetbrains/kotlinx/dataframe/io/db/DbType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;)V + public synthetic fun (Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun buildSqlQueryWithLimit (Ljava/lang/String;I)Ljava/lang/String; public fun buildTableMetadata (Ljava/sql/ResultSet;)Lorg/jetbrains/kotlinx/dataframe/io/db/TableMetadata; public fun convertSqlTypeToColumnSchemaValue (Lorg/jetbrains/kotlinx/dataframe/io/db/TableColumnMetadata;)Lorg/jetbrains/kotlinx/dataframe/schema/ColumnSchema; public fun convertSqlTypeToKType (Lorg/jetbrains/kotlinx/dataframe/io/db/TableColumnMetadata;)Lkotlin/reflect/KType; - public final fun getDialect ()Lorg/jetbrains/kotlinx/dataframe/io/db/DbType; public fun getDriverClassName ()Ljava/lang/String; + public final fun getMode ()Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; public fun isSystemTable (Lorg/jetbrains/kotlinx/dataframe/io/db/TableMetadata;)Z } public final class org/jetbrains/kotlinx/dataframe/io/db/H2$Companion { } +public final class org/jetbrains/kotlinx/dataframe/io/db/H2$Mode : java/lang/Enum { + public static final field Companion Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode$Companion; + public static final field MariaDb Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; + public static final field MsSqlServer Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; + public static final field MySql Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; + public static final field PostgreSql Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; + public static final field Regular Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getValue ()Ljava/lang/String; + public final fun toDbType ()Lorg/jetbrains/kotlinx/dataframe/io/db/DbType; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; + public static fun values ()[Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; +} + +public final class org/jetbrains/kotlinx/dataframe/io/db/H2$Mode$Companion { + public final fun fromDbType (Lorg/jetbrains/kotlinx/dataframe/io/db/DbType;)Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; + public final fun fromString (Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; + public final fun fromValue (Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; +} + public final class org/jetbrains/kotlinx/dataframe/io/db/MariaDb : org/jetbrains/kotlinx/dataframe/io/db/DbType { public static final field INSTANCE Lorg/jetbrains/kotlinx/dataframe/io/db/MariaDb; public fun buildTableMetadata (Ljava/sql/ResultSet;)Lorg/jetbrains/kotlinx/dataframe/io/db/TableMetadata; From 4ee8f0431d6bf9293dad43e313c48816370fc32f Mon Sep 17 00:00:00 2001 From: zaleslaw Date: Tue, 2 Dec 2025 13:10:26 +0100 Subject: [PATCH 5/5] Api Dump + Linter --- dataframe-jdbc/api/dataframe-jdbc.api | 1 - .../jetbrains/kotlinx/dataframe/io/db/H2.kt | 13 --- .../jetbrains/kotlinx/dataframe/io/db/util.kt | 2 +- .../kotlinx/dataframe/io/h2/h2Test.kt | 94 +++++++++++++++++++ .../StardustDocs/topics/dataSources/sql/H2.md | 2 +- 5 files changed, 96 insertions(+), 16 deletions(-) diff --git a/dataframe-jdbc/api/dataframe-jdbc.api b/dataframe-jdbc/api/dataframe-jdbc.api index 24ecebd50e..bb142cba42 100644 --- a/dataframe-jdbc/api/dataframe-jdbc.api +++ b/dataframe-jdbc/api/dataframe-jdbc.api @@ -165,7 +165,6 @@ public final class org/jetbrains/kotlinx/dataframe/io/db/H2$Mode : java/lang/Enu public final class org/jetbrains/kotlinx/dataframe/io/db/H2$Mode$Companion { public final fun fromDbType (Lorg/jetbrains/kotlinx/dataframe/io/db/DbType;)Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; - public final fun fromString (Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; public final fun fromValue (Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode; } diff --git a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt index aaa50226a5..96cea43724 100644 --- a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt +++ b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/H2.kt @@ -88,19 +88,6 @@ public open class H2(public val mode: Mode = Mode.Regular) : DbType("h2") { } return entries.find { it.value.equals(value, ignoreCase = true) } } - - /** - * Parses a string that may be an H2 MODE value into a Mode. - * Accepts case-insensitive `regular` and `h2-regular` as Regular. - * - * @param mode The mode string to parse, or null for Regular mode. - * @return The corresponding Mode for null/empty input or supported modes. - * @throws IllegalArgumentException if the mode is not null and not supported. - */ - public fun fromString(mode: String?): Mode? { - if (mode == null) return null - return fromValue(mode) - } } } diff --git a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt index a0a83d6fa0..bea3f53d77 100644 --- a/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt +++ b/dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt @@ -67,7 +67,7 @@ private fun fetchH2ModeFromConnection(connection: Connection): String? { /** * Parses H2 mode string and returns the corresponding H2 DbType instance. * - * @param [mode] the mode string (maybe null or empty for Regular mode). + * @param [mode] the mode string (may be null or empty for Regular mode). * @return H2 instance with the appropriate mode. * @throws [IllegalArgumentException] if the mode is not supported. */ diff --git a/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt b/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt index 4a35ad4eb1..40c09fd04a 100644 --- a/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt +++ b/dataframe-jdbc/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2/h2Test.kt @@ -5,6 +5,7 @@ import com.zaxxer.hikari.HikariDataSource import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import org.intellij.lang.annotations.Language import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.DataFrame @@ -16,6 +17,13 @@ import org.jetbrains.kotlinx.dataframe.api.select import org.jetbrains.kotlinx.dataframe.io.DbConnectionConfig import org.jetbrains.kotlinx.dataframe.io.db.H2 import org.jetbrains.kotlinx.dataframe.io.db.H2.Mode +import org.jetbrains.kotlinx.dataframe.io.db.MySql +import org.jetbrains.kotlinx.dataframe.io.db.PostgreSql +import org.jetbrains.kotlinx.dataframe.io.db.Sqlite +import org.jetbrains.kotlinx.dataframe.io.db.TableMetadata +import org.jetbrains.kotlinx.dataframe.io.db.driverClassNameFromUrl +import org.jetbrains.kotlinx.dataframe.io.db.extractDBTypeFromConnection +import org.jetbrains.kotlinx.dataframe.io.db.extractDBTypeFromUrl import org.jetbrains.kotlinx.dataframe.io.inferNullability import org.jetbrains.kotlinx.dataframe.io.readAllSqlTables import org.jetbrains.kotlinx.dataframe.io.readDataFrame @@ -1076,6 +1084,92 @@ class JdbcTest { } } + @Test + fun `H2 Regular mode extraction and fallbacks`() { + // 1. Create a connection without explicit MODE in URL. + // H2 defaults to Regular mode. extractDBTypeFromConnection should detect this by querying settings. + DriverManager.getConnection("jdbc:h2:mem:testRegularFallback").use { conn -> + val dbType = extractDBTypeFromConnection(conn) + + (dbType is H2) shouldBe true + (dbType as H2).mode shouldBe Mode.Regular + + // 2. Verify fallback behaviors (when delegate is null) + + // buildSqlQueryWithLimit: Check fallback to super implementation (standard LIMIT syntax) + val query = "SELECT * FROM table" + dbType.buildSqlQueryWithLimit(query, 10) shouldBe "SELECT * FROM table LIMIT 10" + + // isSystemTable: Check fallback to H2-specific logic (INFORMATION_SCHEMA) + val systemTable = TableMetadata("SETTINGS", "INFORMATION_SCHEMA", "TEST_DB") + dbType.isSystemTable(systemTable) shouldBe true + + val userTable = TableMetadata("USERS", "PUBLIC", "TEST_DB") + dbType.isSystemTable(userTable) shouldBe false + + // buildTableMetadata: Check fallback to reading from ResultSet directly + conn.createStatement().use { st -> + st.execute("CREATE TABLE MY_FALLBACK_TABLE (ID INT)") + } + conn.metaData.getTables(null, null, "MY_FALLBACK_TABLE", null).use { rs -> + if (rs.next()) { + val metadata = dbType.buildTableMetadata(rs) + metadata.name shouldBe "MY_FALLBACK_TABLE" + metadata.schemaName shouldBe "PUBLIC" + metadata.catalogue shouldNotBe null + } else { + throw IllegalStateException("Could not find created table metadata") + } + } + } + } + + @Test + fun `database type extraction utils`() { + // 1. Test direct extraction from URL for various DBs + (extractDBTypeFromUrl("jdbc:mysql://localhost:3306/db") is MySql) shouldBe true + (extractDBTypeFromUrl("jdbc:postgresql://localhost:5432/db") is PostgreSql) shouldBe true + (extractDBTypeFromUrl("jdbc:sqlite:sample.db") is Sqlite) shouldBe true + + // Test driverClassNameFromUrl + driverClassNameFromUrl("jdbc:mysql://localhost:3306/db") shouldBe "com.mysql.jdbc.Driver" + driverClassNameFromUrl("jdbc:postgresql://localhost:5432/db") shouldBe "org.postgresql.Driver" + driverClassNameFromUrl("jdbc:h2:mem:test") shouldBe "org.h2.Driver" + + // 2. Test unsupported Database URL + shouldThrow { + extractDBTypeFromUrl("jdbc:oracle:thin:@localhost:1521:xe") + } + + // 3. Test null URL + shouldThrow { + extractDBTypeFromUrl(null) + } + + // 4. Test H2 specific mode extraction from Connection (End-to-End) + + // Case A: MySQL Mode via URL + DriverManager.getConnection("jdbc:h2:mem:testExtractMySql;MODE=MySQL").use { conn -> + val dbType = extractDBTypeFromConnection(conn) + (dbType is H2) shouldBe true + (dbType as H2).mode shouldBe H2.Mode.MySql + } + + // Case B: PostgreSQL Mode via URL + DriverManager.getConnection("jdbc:h2:mem:testExtractPostgres;MODE=PostgreSQL").use { conn -> + val dbType = extractDBTypeFromConnection(conn) + (dbType is H2) shouldBe true + (dbType as H2).mode shouldBe H2.Mode.PostgreSql + } + + // Case C: MSSQLServer Mode via URL + DriverManager.getConnection("jdbc:h2:mem:testExtractMsSql;MODE=MSSQLServer").use { conn -> + val dbType = extractDBTypeFromConnection(conn) + (dbType is H2) shouldBe true + (dbType as H2).mode shouldBe H2.Mode.MsSqlServer + } + } + // helper object created for API testing purposes object CustomDB : H2(Mode.MySql) diff --git a/docs/StardustDocs/topics/dataSources/sql/H2.md b/docs/StardustDocs/topics/dataSources/sql/H2.md index 4f0f228c2d..bc57d64488 100644 --- a/docs/StardustDocs/topics/dataSources/sql/H2.md +++ b/docs/StardustDocs/topics/dataSources/sql/H2.md @@ -72,7 +72,7 @@ val url = "jdbc:h2:mem:testDatabase" val username = "sa" val password = "" -val dbConfig = DbConnectionConfig(url, "sa", "") +val dbConfig = DbConnectionConfig(url, username, password) val tableName = "Customer"