Skip to content

Commit 4ee79ed

Browse files
authored
The KING of Reverts (#1598)
* Revert "Revert "Introduce `Mode` enum for H2 compatibility modes with deprecation of old constants"" This reverts commit 63ecf8a. * Revert "Revert "Add enhanced support for H2 compatibility modes via `Mode` enumeration and refine database handling"" This reverts commit 0037070. * 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. * Api Dump * Api Dump + Linter
1 parent b730340 commit 4ee79ed

File tree

5 files changed

+421
-100
lines changed

5 files changed

+421
-100
lines changed

dataframe-jdbc/api/dataframe-jdbc.api

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,19 +135,39 @@ public class org/jetbrains/kotlinx/dataframe/io/db/H2 : org/jetbrains/kotlinx/da
135135
public static final field MODE_POSTGRESQL Ljava/lang/String;
136136
public fun <init> ()V
137137
public fun <init> (Lorg/jetbrains/kotlinx/dataframe/io/db/DbType;)V
138-
public synthetic fun <init> (Lorg/jetbrains/kotlinx/dataframe/io/db/DbType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
138+
public fun <init> (Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;)V
139+
public synthetic fun <init> (Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
139140
public fun buildSqlQueryWithLimit (Ljava/lang/String;I)Ljava/lang/String;
140141
public fun buildTableMetadata (Ljava/sql/ResultSet;)Lorg/jetbrains/kotlinx/dataframe/io/db/TableMetadata;
141142
public fun convertSqlTypeToColumnSchemaValue (Lorg/jetbrains/kotlinx/dataframe/io/db/TableColumnMetadata;)Lorg/jetbrains/kotlinx/dataframe/schema/ColumnSchema;
142143
public fun convertSqlTypeToKType (Lorg/jetbrains/kotlinx/dataframe/io/db/TableColumnMetadata;)Lkotlin/reflect/KType;
143-
public final fun getDialect ()Lorg/jetbrains/kotlinx/dataframe/io/db/DbType;
144144
public fun getDriverClassName ()Ljava/lang/String;
145+
public final fun getMode ()Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;
145146
public fun isSystemTable (Lorg/jetbrains/kotlinx/dataframe/io/db/TableMetadata;)Z
146147
}
147148

148149
public final class org/jetbrains/kotlinx/dataframe/io/db/H2$Companion {
149150
}
150151

152+
public final class org/jetbrains/kotlinx/dataframe/io/db/H2$Mode : java/lang/Enum {
153+
public static final field Companion Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode$Companion;
154+
public static final field MariaDb Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;
155+
public static final field MsSqlServer Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;
156+
public static final field MySql Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;
157+
public static final field PostgreSql Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;
158+
public static final field Regular Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;
159+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
160+
public final fun getValue ()Ljava/lang/String;
161+
public final fun toDbType ()Lorg/jetbrains/kotlinx/dataframe/io/db/DbType;
162+
public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;
163+
public static fun values ()[Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;
164+
}
165+
166+
public final class org/jetbrains/kotlinx/dataframe/io/db/H2$Mode$Companion {
167+
public final fun fromDbType (Lorg/jetbrains/kotlinx/dataframe/io/db/DbType;)Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;
168+
public final fun fromValue (Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/io/db/H2$Mode;
169+
}
170+
151171
public final class org/jetbrains/kotlinx/dataframe/io/db/MariaDb : org/jetbrains/kotlinx/dataframe/io/db/DbType {
152172
public static final field INSTANCE Lorg/jetbrains/kotlinx/dataframe/io/db/MariaDb;
153173
public fun buildTableMetadata (Ljava/sql/ResultSet;)Lorg/jetbrains/kotlinx/dataframe/io/db/TableMetadata;

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

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import org.jetbrains.kotlinx.dataframe.schema.ColumnSchema
44
import java.sql.ResultSet
55
import java.util.Locale
66
import kotlin.reflect.KType
7+
import org.jetbrains.kotlinx.dataframe.io.db.MariaDb as MariaDbType
8+
import org.jetbrains.kotlinx.dataframe.io.db.MsSql as MsSqlType
9+
import org.jetbrains.kotlinx.dataframe.io.db.MySql as MySqlType
10+
import org.jetbrains.kotlinx.dataframe.io.db.PostgreSql as PostgreSqlType
711

812
/**
913
* Represents the H2 database type.
@@ -13,9 +17,78 @@ 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!" }
20+
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? =
49+
when (this) {
50+
Regular -> null
51+
MySql -> MySqlType
52+
PostgreSql -> PostgreSqlType
53+
MsSqlServer -> MsSqlType
54+
MariaDb -> MariaDbType
55+
}
56+
57+
public companion object {
58+
/**
59+
* Creates a Mode from the given DbType.
60+
*
61+
* @param dialect The DbType to convert.
62+
* @return The corresponding Mode, or null if the dialect is H2.
63+
*/
64+
public fun fromDbType(dialect: DbType): Mode? =
65+
when (dialect) {
66+
is H2 -> null
67+
MySqlType -> MySql
68+
PostgreSqlType -> PostgreSql
69+
MsSqlType -> MsSqlServer
70+
MariaDbType -> MariaDb
71+
else -> Regular
72+
}
73+
74+
/**
75+
* Finds a Mode by its string value (case-insensitive).
76+
* Handles both URL values (MySQL, PostgreSQL, etc.) and
77+
* INFORMATION_SCHEMA values (Regular).
78+
*
79+
* @param value The string value to search for.
80+
* @return The matching Mode, or null if not found.
81+
*/
82+
public fun fromValue(value: String): Mode? {
83+
// "Regular" from INFORMATION_SCHEMA or "H2-Regular" from URL
84+
if (value.equals("regular", ignoreCase = true) ||
85+
value.equals("h2-regular", ignoreCase = true)
86+
) {
87+
return Regular
88+
}
89+
return entries.find { it.value.equals(value, ignoreCase = true) }
90+
}
91+
}
1992
}
2093

2194
/**
@@ -29,24 +102,25 @@ public open class H2(public val dialect: DbType = MySql) : DbType("h2") {
29102
* @see [createH2Instance]
30103
*/
31104
public companion object {
32-
/** It represents the mode value "MySQL" for the H2 database. */
105+
106+
@Deprecated("Use Mode.MySql.value instead", ReplaceWith("Mode.MySql.value"))
33107
public const val MODE_MYSQL: String = "MySQL"
34108

35-
/** It represents the mode value "PostgreSQL" for the H2 database. */
109+
@Deprecated("Use Mode.PostgreSql.value instead", ReplaceWith("Mode.PostgreSql.value"))
36110
public const val MODE_POSTGRESQL: String = "PostgreSQL"
37111

38-
/** It represents the mode value "MSSQLServer" for the H2 database. */
112+
@Deprecated("Use Mode.MsSqlServer.value instead", ReplaceWith("Mode.MsSqlServer.value"))
39113
public const val MODE_MSSQLSERVER: String = "MSSQLServer"
40114

41-
/** It represents the mode value "MariaDB" for the H2 database. */
115+
@Deprecated("Use Mode.MariaDb.value instead", ReplaceWith("Mode.MariaDb.value"))
42116
public const val MODE_MARIADB: String = "MariaDB"
43117
}
44118

45119
override val driverClassName: String
46120
get() = "org.h2.Driver"
47121

48122
override fun convertSqlTypeToColumnSchemaValue(tableColumnMetadata: TableColumnMetadata): ColumnSchema? =
49-
dialect.convertSqlTypeToColumnSchemaValue(tableColumnMetadata)
123+
delegate?.convertSqlTypeToColumnSchemaValue(tableColumnMetadata)
50124

51125
override fun isSystemTable(tableMetadata: TableMetadata): Boolean {
52126
val locale = Locale.getDefault()
@@ -57,14 +131,24 @@ public open class H2(public val dialect: DbType = MySql) : DbType("h2") {
57131
// could be extended for other symptoms of the system tables for H2
58132
val isH2SystemTable = schemaName.containsWithLowercase("information_schema")
59133

60-
return isH2SystemTable || dialect.isSystemTable(tableMetadata)
134+
return if (delegate == null) {
135+
isH2SystemTable
136+
} else {
137+
isH2SystemTable || delegate.isSystemTable(tableMetadata)
138+
}
61139
}
62140

63-
override fun buildTableMetadata(tables: ResultSet): TableMetadata = dialect.buildTableMetadata(tables)
141+
override fun buildTableMetadata(tables: ResultSet): TableMetadata =
142+
delegate?.buildTableMetadata(tables)
143+
?: TableMetadata(
144+
tables.getString("table_name"),
145+
tables.getString("table_schem"),
146+
tables.getString("table_cat"),
147+
)
64148

65149
override fun convertSqlTypeToKType(tableColumnMetadata: TableColumnMetadata): KType? =
66-
dialect.convertSqlTypeToKType(tableColumnMetadata)
150+
delegate?.convertSqlTypeToKType(tableColumnMetadata)
67151

68152
public override fun buildSqlQueryWithLimit(sqlQuery: String, limit: Int): String =
69-
dialect.buildSqlQueryWithLimit(sqlQuery, limit)
153+
delegate?.buildSqlQueryWithLimit(sqlQuery, limit) ?: super.buildSqlQueryWithLimit(sqlQuery, limit)
70154
}

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

Lines changed: 72 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@ package org.jetbrains.kotlinx.dataframe.io.db
33
import io.github.oshai.kotlinlogging.KotlinLogging
44
import java.sql.Connection
55
import java.sql.SQLException
6-
import java.util.Locale
76

87
private val logger = KotlinLogging.logger {}
98

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

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)
32+
// First, determine the base database type from URL
33+
val baseDbType = extractDBTypeFromUrl(url)
4234

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 }
35+
// For H2, refine the mode by querying the active connection settings
36+
// This handles cases where MODE is not specified in URL, but H2 returns "Regular" from settings
37+
return if (baseDbType is H2) {
38+
val mode = fetchH2ModeFromConnection(connection)
39+
parseH2ModeOrThrow(mode)
40+
} else {
41+
logger.info { "Identified DB type as $baseDbType from url: $url" }
42+
baseDbType
43+
}
44+
}
5345

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

6484
/**
6585
* Extracts the database type from the given JDBC URL.
6686
*
6787
* @param [url] the JDBC URL.
6888
* @return the corresponding [DbType].
69-
* @throws [RuntimeException] if the url is null.
89+
* @throws [SQLException] if the url is null.
90+
* @throws [IllegalArgumentException] if the URL specifies an unsupported database type.
7091
*/
7192
public fun extractDBTypeFromUrl(url: String?): DbType {
72-
if (url != null) {
73-
val helperH2Instance = H2()
74-
return when {
75-
helperH2Instance.dbTypeInJdbcUrl in url -> createH2Instance(url)
93+
url ?: throw SQLException("Database URL could not be null.")
7694

77-
MariaDb.dbTypeInJdbcUrl in url -> MariaDb
95+
return when {
96+
H2().dbTypeInJdbcUrl in url -> createH2Instance(url)
7897

79-
MySql.dbTypeInJdbcUrl in url -> MySql
98+
MariaDb.dbTypeInJdbcUrl in url -> MariaDb
8099

81-
Sqlite.dbTypeInJdbcUrl in url -> Sqlite
100+
MySql.dbTypeInJdbcUrl in url -> MySql
82101

83-
PostgreSql.dbTypeInJdbcUrl in url -> PostgreSql
102+
Sqlite.dbTypeInJdbcUrl in url -> Sqlite
84103

85-
MsSql.dbTypeInJdbcUrl in url -> MsSql
104+
PostgreSql.dbTypeInJdbcUrl in url -> PostgreSql
86105

87-
DuckDb.dbTypeInJdbcUrl in url -> DuckDb
106+
MsSql.dbTypeInJdbcUrl in url -> MsSql
88107

89-
else -> throw IllegalArgumentException(
90-
"Unsupported database type in the url: $url. " +
91-
"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")
108+
DuckDb.dbTypeInJdbcUrl in url -> DuckDb
109+
110+
else -> throw IllegalArgumentException(
111+
"Unsupported database type in the url: $url. " +
112+
"Only H2, MariaDB, MySQL, MSSQL, SQLite, PostgreSQL, and DuckDB are supported!",
113+
)
96114
}
97115
}
98116

@@ -104,30 +122,8 @@ public fun extractDBTypeFromUrl(url: String?): DbType {
104122
* @throws [IllegalArgumentException] if the provided URL does not contain a valid mode.
105123
*/
106124
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-
}
125+
val mode = H2_MODE_URL_PATTERN.find(url)?.groupValues?.getOrNull(1)
126+
return parseH2ModeOrThrow(mode?.takeIf { it.isNotBlank() })
131127
}
132128

133129
/**

0 commit comments

Comments
 (0)