Skip to content

Commit dcaab7c

Browse files
committed
Add enhanced support for H2 compatibility modes via Mode enumeration and refine database handling
Refactored `H2` implementation to use a `Mode` enum encapsulating compatibility modes like `MySql`, `PostgreSQL`, and `MSSQLServer`, deprecating prior static constants. Updated related factory methods, tests, and documentation to align with the new design. Improved H2 mode parsing from both URL and connection metadata, adding validation for unsupported modes.
1 parent d4aab31 commit dcaab7c

File tree

4 files changed

+294
-133
lines changed
  • dataframe-jdbc/src
    • main/kotlin/org/jetbrains/kotlinx/dataframe/io/db
    • test/kotlin/org/jetbrains/kotlinx/dataframe/io/h2
  • docs/StardustDocs/topics/dataSources/sql

4 files changed

+294
-133
lines changed
Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package org.jetbrains.kotlinx.dataframe.io.db
22

33
import org.jetbrains.kotlinx.dataframe.schema.ColumnSchema
4+
import org.jetbrains.kotlinx.dataframe.io.db.MariaDb as MariaDbType
5+
import org.jetbrains.kotlinx.dataframe.io.db.MsSql as MsSqlType
6+
import org.jetbrains.kotlinx.dataframe.io.db.MySql as MySqlType
7+
import org.jetbrains.kotlinx.dataframe.io.db.PostgreSql as PostgreSqlType
48
import java.sql.ResultSet
59
import java.util.Locale
610
import kotlin.reflect.KType
@@ -13,12 +17,89 @@ import kotlin.reflect.KType
1317
*
1418
* NOTE: All date and timestamp-related types are converted to String to avoid java.sql.* types.
1519
*/
16-
public open class H2(public val dialect: DbType = MySql) : DbType("h2") {
17-
init {
18-
require(dialect::class != H2::class) { "H2 database could not be specified with H2 dialect!" }
19-
}
2020

21+
public open class H2(public val mode: Mode = Mode.Regular) : DbType("h2") {
22+
@Deprecated("Use H2(mode = Mode.XXX) instead", ReplaceWith("H2(H2.Mode.MySql)"))
23+
public constructor(dialect: DbType) : this(
24+
Mode.fromDbType(dialect)
25+
?: throw IllegalArgumentException("H2 database could not be specified with H2 dialect!"),
26+
)
27+
28+
private val delegate: DbType? = mode.toDbType()
29+
30+
/**
31+
* Represents the compatibility modes supported by an H2 database.
32+
*
33+
* @property value The string value used in H2 JDBC URL and settings.
34+
*/
35+
public enum class Mode(public val value: String) {
36+
/** Native H2 mode (no compatibility), our synthetic marker. */
37+
Regular("H2-Regular"),
38+
MySql("MySQL"),
39+
PostgreSql("PostgreSQL"),
40+
MsSqlServer("MSSQLServer"),
41+
MariaDb("MariaDB");
42+
43+
/**
44+
* Converts this Mode to the corresponding DbType delegate.
45+
*
46+
* @return The DbType for this mode, or null for Regular mode.
47+
*/
48+
public fun toDbType(): DbType? = when (this) {
49+
Regular -> null
50+
MySql -> MySqlType
51+
PostgreSql -> PostgreSqlType
52+
MsSqlServer -> MsSqlType
53+
MariaDb -> MariaDbType
54+
}
55+
56+
public companion object {
57+
/**
58+
* Creates a Mode from the given DbType.
59+
*
60+
* @param dialect The DbType to convert.
61+
* @return The corresponding Mode, or null if the dialect is H2.
62+
*/
63+
public fun fromDbType(dialect: DbType): Mode? = when (dialect) {
64+
is H2 -> null
65+
MySqlType -> MySql
66+
PostgreSqlType -> PostgreSql
67+
MsSqlType -> MsSqlServer
68+
MariaDbType -> MariaDb
69+
else -> Regular
70+
}
71+
72+
/**
73+
* Finds a Mode by its string value (case-insensitive).
74+
* Handles both URL values (MySQL, PostgreSQL, etc.) and
75+
* INFORMATION_SCHEMA values (Regular).
76+
*
77+
* @param value The string value to search for.
78+
* @return The matching Mode, or null if not found.
79+
*/
80+
public fun fromValue(value: String): Mode? {
81+
// "Regular" from INFORMATION_SCHEMA or "H2-Regular" from URL
82+
if (value.equals("regular", ignoreCase = true) ||
83+
value.equals("h2-regular", ignoreCase = true)) {
84+
return Regular
85+
}
86+
return entries.find { it.value.equals(value, ignoreCase = true) }
87+
}
2188

89+
/**
90+
* Parses a string that may be an H2 MODE value into a Mode.
91+
* Accepts case-insensitive `regular` and `h2-regular` as Regular.
92+
*
93+
* @param mode The mode string to parse, or null for Regular mode.
94+
* @return The corresponding Mode for null/empty input or supported modes.
95+
* @throws IllegalArgumentException if the mode is not null and not supported.
96+
*/
97+
public fun fromString(mode: String?): Mode? {
98+
if (mode == null) return null
99+
return fromValue(mode)
100+
}
101+
}
102+
}
22103
/**
23104
* It contains constants related to different database modes.
24105
*
@@ -30,28 +111,6 @@ public open class H2(public val dialect: DbType = MySql) : DbType("h2") {
30111
* @see [createH2Instance]
31112
*/
32113
public companion object {
33-
/**
34-
* Represents the compatibility modes supported by an H2 database.
35-
*
36-
* @property value The string value used in H2 JDBC URL and settings.
37-
*/
38-
public enum class Mode(public val value: String) {
39-
MySql("MySQL"),
40-
PostgreSql("PostgreSQL"),
41-
MsSqlServer("MSSQLServer"),
42-
MariaDb("MariaDB");
43-
44-
public companion object {
45-
/**
46-
* Finds a Mode by its string value (case-insensitive).
47-
*
48-
* @param value The string value to search for.
49-
* @return The matching Mode, or null if not found.
50-
*/
51-
public fun fromValue(value: String): Mode? =
52-
entries.find { it.value.equals(value, ignoreCase = true) }
53-
}
54-
}
55114

56115
@Deprecated("Use Mode.MySql.value instead", ReplaceWith("Mode.MySql.value"))
57116
public const val MODE_MYSQL: String = "MySQL"
@@ -67,7 +126,7 @@ public open class H2(public val dialect: DbType = MySql) : DbType("h2") {
67126
get() = "org.h2.Driver"
68127

69128
override fun convertSqlTypeToColumnSchemaValue(tableColumnMetadata: TableColumnMetadata): ColumnSchema? =
70-
dialect.convertSqlTypeToColumnSchemaValue(tableColumnMetadata)
129+
delegate?.convertSqlTypeToColumnSchemaValue(tableColumnMetadata)
71130

72131
override fun isSystemTable(tableMetadata: TableMetadata): Boolean {
73132
val locale = Locale.getDefault()
@@ -78,14 +137,24 @@ public open class H2(public val dialect: DbType = MySql) : DbType("h2") {
78137
// could be extended for other symptoms of the system tables for H2
79138
val isH2SystemTable = schemaName.containsWithLowercase("information_schema")
80139

81-
return isH2SystemTable || dialect.isSystemTable(tableMetadata)
140+
return if (delegate == null) {
141+
isH2SystemTable
142+
} else {
143+
isH2SystemTable || delegate.isSystemTable(tableMetadata)
144+
}
82145
}
83146

84-
override fun buildTableMetadata(tables: ResultSet): TableMetadata = dialect.buildTableMetadata(tables)
147+
override fun buildTableMetadata(tables: ResultSet): TableMetadata =
148+
delegate?.buildTableMetadata(tables)
149+
?: TableMetadata(
150+
tables.getString("table_name"),
151+
tables.getString("table_schem"),
152+
tables.getString("table_cat"),
153+
)
85154

86155
override fun convertSqlTypeToKType(tableColumnMetadata: TableColumnMetadata): KType? =
87-
dialect.convertSqlTypeToKType(tableColumnMetadata)
156+
delegate?.convertSqlTypeToKType(tableColumnMetadata)
88157

89158
public override fun buildSqlQueryWithLimit(sqlQuery: String, limit: Int): String =
90-
dialect.buildSqlQueryWithLimit(sqlQuery, limit)
159+
delegate?.buildSqlQueryWithLimit(sqlQuery, limit) ?: super.buildSqlQueryWithLimit(sqlQuery, limit)
91160
}

dataframe-jdbc/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/db/util.kt

Lines changed: 71 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@ import java.util.Locale
77

88
private val logger = KotlinLogging.logger {}
99

10+
private const val UNSUPPORTED_H2_MODE_MESSAGE =
11+
"Unsupported H2 MODE: %s. Supported: MySQL, PostgreSQL, MSSQLServer, MariaDB, REGULAR/H2-Regular (or omit MODE)."
12+
13+
private const val H2_MODE_QUERY = "SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE'"
14+
15+
private val H2_MODE_URL_PATTERN = "MODE=([^;:&]+)".toRegex(RegexOption.IGNORE_CASE)
16+
1017
/**
1118
* Extracts the database type from the given connection.
19+
* For H2, fetches the actual MODE from the active connection settings.
20+
* For other databases, extracts type from URL.
1221
*
1322
* @param [connection] the database connection.
1423
* @return the corresponding [DbType].
@@ -21,78 +30,81 @@ public fun extractDBTypeFromConnection(connection: Connection): DbType {
2130
?: throw IllegalStateException("URL information is missing in connection meta data!")
2231
logger.info { "Processing DB type extraction for connection url: $url" }
2332

24-
return if (url.contains(H2().dbTypeInJdbcUrl)) {
25-
// works only for H2 version 2
26-
val modeQuery = "SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE'"
27-
var mode = ""
28-
connection.prepareStatement(modeQuery).use { st ->
29-
st.executeQuery().use { rs ->
30-
if (rs.next()) {
31-
mode = rs.getString("SETTING_VALUE")
32-
logger.debug { "Fetched H2 DB mode: $mode" }
33-
} else {
34-
throw IllegalStateException("The information about H2 mode is not found in the H2 meta-data!")
35-
}
36-
}
37-
}
38-
39-
// H2 doesn't support MariaDB and SQLite
40-
when (mode.lowercase(Locale.getDefault())) {
41-
H2.MODE_MYSQL.lowercase(Locale.getDefault()) -> H2(MySql)
33+
// First, determine the base database type from URL
34+
val baseDbType = extractDBTypeFromUrl(url)
4235

43-
H2.MODE_MSSQLSERVER.lowercase(Locale.getDefault()) -> H2(MsSql)
44-
45-
H2.MODE_POSTGRESQL.lowercase(Locale.getDefault()) -> H2(PostgreSql)
46-
47-
H2.MODE_MARIADB.lowercase(Locale.getDefault()) -> H2(MariaDb)
48-
49-
else -> {
50-
val message = "Unsupported database type in the url: $url. " +
51-
"Only MySQL, MariaDB, MSSQL and PostgreSQL are supported!"
52-
logger.error { message }
36+
// For H2, refine the mode by querying the active connection settings
37+
// This handles cases where MODE is not specified in URL, but H2 returns "Regular" from settings
38+
return if (baseDbType is H2) {
39+
val mode = fetchH2ModeFromConnection(connection)
40+
parseH2ModeOrThrow(mode)
41+
} else {
42+
logger.info { "Identified DB type as $baseDbType from url: $url" }
43+
baseDbType
44+
}
45+
}
5346

54-
throw IllegalArgumentException(message)
47+
/**
48+
* Fetches H2 database mode from an active connection.
49+
* Works only for H2 version 2.
50+
*
51+
* @param [connection] the database connection.
52+
* @return the mode string or null if not set.
53+
*/
54+
private fun fetchH2ModeFromConnection(connection: Connection): String? {
55+
var mode: String? = null
56+
connection.prepareStatement(H2_MODE_QUERY).use { st ->
57+
st.executeQuery().use { rs ->
58+
if (rs.next()) {
59+
mode = rs.getString("SETTING_VALUE")
60+
logger.debug { "Fetched H2 DB mode: $mode" }
5561
}
5662
}
57-
} else {
58-
val dbType = extractDBTypeFromUrl(url)
59-
logger.info { "Identified DB type as $dbType from url: $url" }
60-
dbType
6163
}
64+
65+
return mode?.trim()?.takeIf { it.isNotEmpty() }
66+
}
67+
68+
/**
69+
* Parses H2 mode string and returns the corresponding H2 DbType instance.
70+
*
71+
* @param [mode] the mode string (maybe null or empty for Regular mode).
72+
* @return H2 instance with the appropriate mode.
73+
* @throws [IllegalArgumentException] if the mode is not supported.
74+
*/
75+
private fun parseH2ModeOrThrow(mode: String?): H2 {
76+
if (mode.isNullOrEmpty()) {
77+
return H2(H2.Mode.Regular)
78+
}
79+
return H2.Mode.fromValue(mode)?.let { H2(it) }
80+
?: throw IllegalArgumentException(UNSUPPORTED_H2_MODE_MESSAGE.format(mode)).also {
81+
logger.error { it.message }
82+
}
6283
}
6384

6485
/**
6586
* Extracts the database type from the given JDBC URL.
6687
*
6788
* @param [url] the JDBC URL.
6889
* @return the corresponding [DbType].
69-
* @throws [RuntimeException] if the url is null.
90+
* @throws [SQLException] if the url is null.
91+
* @throws [IllegalArgumentException] if the URL specifies an unsupported database type.
7092
*/
7193
public fun extractDBTypeFromUrl(url: String?): DbType {
72-
if (url != null) {
73-
val helperH2Instance = H2()
74-
return when {
75-
helperH2Instance.dbTypeInJdbcUrl in url -> createH2Instance(url)
76-
77-
MariaDb.dbTypeInJdbcUrl in url -> MariaDb
78-
79-
MySql.dbTypeInJdbcUrl in url -> MySql
80-
81-
Sqlite.dbTypeInJdbcUrl in url -> Sqlite
82-
83-
PostgreSql.dbTypeInJdbcUrl in url -> PostgreSql
84-
85-
MsSql.dbTypeInJdbcUrl in url -> MsSql
86-
87-
DuckDb.dbTypeInJdbcUrl in url -> DuckDb
88-
89-
else -> throw IllegalArgumentException(
90-
"Unsupported database type in the url: $url. " +
94+
url ?: throw SQLException("Database URL could not be null.")
95+
96+
return when {
97+
H2().dbTypeInJdbcUrl in url -> createH2Instance(url)
98+
MariaDb.dbTypeInJdbcUrl in url -> MariaDb
99+
MySql.dbTypeInJdbcUrl in url -> MySql
100+
Sqlite.dbTypeInJdbcUrl in url -> Sqlite
101+
PostgreSql.dbTypeInJdbcUrl in url -> PostgreSql
102+
MsSql.dbTypeInJdbcUrl in url -> MsSql
103+
DuckDb.dbTypeInJdbcUrl in url -> DuckDb
104+
else -> throw IllegalArgumentException(
105+
"Unsupported database type in the url: $url. " +
91106
"Only H2, MariaDB, MySQL, MSSQL, SQLite, PostgreSQL, and DuckDB are supported!",
92-
)
93-
}
94-
} else {
95-
throw SQLException("Database URL could not be null. The existing value is $url")
107+
)
96108
}
97109
}
98110

@@ -104,30 +116,8 @@ public fun extractDBTypeFromUrl(url: String?): DbType {
104116
* @throws [IllegalArgumentException] if the provided URL does not contain a valid mode.
105117
*/
106118
private fun createH2Instance(url: String): DbType {
107-
val modePattern = "MODE=(.*?);".toRegex()
108-
val matchResult = modePattern.find(url)
109-
110-
val mode: String = if (matchResult != null && matchResult.groupValues.size == 2) {
111-
matchResult.groupValues[1]
112-
} else {
113-
throw IllegalArgumentException("The provided URL `$url` does not contain a valid mode.")
114-
}
115-
116-
// H2 doesn't support MariaDB and SQLite
117-
return when (mode.lowercase(Locale.getDefault())) {
118-
H2.MODE_MYSQL.lowercase(Locale.getDefault()) -> H2(MySql)
119-
120-
H2.MODE_MSSQLSERVER.lowercase(Locale.getDefault()) -> H2(MsSql)
121-
122-
H2.MODE_POSTGRESQL.lowercase(Locale.getDefault()) -> H2(PostgreSql)
123-
124-
H2.MODE_MARIADB.lowercase(Locale.getDefault()) -> H2(MariaDb)
125-
126-
else -> throw IllegalArgumentException(
127-
"Unsupported database mode: $mode. " +
128-
"Only MySQL, MariaDB, MSSQL, PostgreSQL modes are supported!",
129-
)
130-
}
119+
val mode = H2_MODE_URL_PATTERN.find(url)?.groupValues?.getOrNull(1)
120+
return parseH2ModeOrThrow(mode?.takeIf { it.isNotBlank() })
131121
}
132122

133123
/**

0 commit comments

Comments
 (0)