Skip to content

Commit efdbadb

Browse files
authored
Add JDBC credentials extraction from env variables and improve exception handling (#692)
* Add JDBC credentials extraction and improve exception handling This update introduces the ability to extract JDBC credentials from environment variables enhancing security features. Additionally, improvements have been made to the exception handling when generating schema for SQL table or result of SQL query. * Add environment variable support to JDBC credentials Updated the JDBC connection options to interpret the user and password values as keys for system environment variables when extractCredFromEnv is set to true. This change allows more security and flexibility in handling sensitive JDBC credentials. * Refactor code for readability and enhance jdbcOptions documentation
1 parent 0ca67a4 commit efdbadb

File tree

7 files changed

+180
-32
lines changed

7 files changed

+180
-32
lines changed

core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/annotations/ImportDataSchema.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,21 @@ public annotation class CsvOptions(
4949
public val delimiter: Char,
5050
)
5151

52+
/**
53+
* An annotation class that represents options for JDBC connection.
54+
*
55+
* @property [user] The username for the JDBC connection. Default value is an empty string.
56+
* If [extractCredFromEnv] is true, the [user] value will be interpreted as key for system environment variable.
57+
* @property [password] The password for the JDBC connection. Default value is an empty string.
58+
* If [extractCredFromEnv] is true, the [password] value will be interpreted as key for system environment variable.
59+
* @property [extractCredFromEnv] Whether to extract the JDBC credentials from environment variables. Default value is false.
60+
* @property [tableName] The name of the table for the JDBC connection. Default value is an empty string.
61+
* @property [sqlQuery] The SQL query to be executed in the JDBC connection. Default value is an empty string.
62+
*/
5263
public annotation class JdbcOptions(
53-
public val user: String = "", // TODO: I'm not sure about the default parameters
54-
public val password: String = "", // TODO: I'm not sure about the default parameters)
64+
public val user: String = "",
65+
public val password: String = "",
66+
public val extractCredFromEnv: Boolean = false,
5567
public val tableName: String = "",
5668
public val sqlQuery: String = ""
5769
)

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/annotations/ImportDataSchema.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,21 @@ public annotation class CsvOptions(
4949
public val delimiter: Char,
5050
)
5151

52+
/**
53+
* An annotation class that represents options for JDBC connection.
54+
*
55+
* @property [user] The username for the JDBC connection. Default value is an empty string.
56+
* If [extractCredFromEnv] is true, the [user] value will be interpreted as key for system environment variable.
57+
* @property [password] The password for the JDBC connection. Default value is an empty string.
58+
* If [extractCredFromEnv] is true, the [password] value will be interpreted as key for system environment variable.
59+
* @property [extractCredFromEnv] Whether to extract the JDBC credentials from environment variables. Default value is false.
60+
* @property [tableName] The name of the table for the JDBC connection. Default value is an empty string.
61+
* @property [sqlQuery] The SQL query to be executed in the JDBC connection. Default value is an empty string.
62+
*/
5263
public annotation class JdbcOptions(
53-
public val user: String = "", // TODO: I'm not sure about the default parameters
54-
public val password: String = "", // TODO: I'm not sure about the default parameters)
64+
public val user: String = "",
65+
public val password: String = "",
66+
public val extractCredFromEnv: Boolean = false,
5567
public val tableName: String = "",
5668
public val sqlQuery: String = ""
5769
)

plugins/dataframe-gradle-plugin/src/main/kotlin/org/jetbrains/dataframe/gradle/GenerateDataSchemaTask.kt

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,27 +174,53 @@ abstract class GenerateDataSchemaTask : DefaultTask() {
174174
}
175175
}
176176

177+
// TODO: copy pasted from symbol-processor: DataSchemaGenerator, should be refactored somehow
177178
private fun generateSchemaByJdbcOptions(
178179
jdbcOptions: JdbcOptionsDsl,
179180
connection: Connection,
180181
): DataFrameSchema {
181182
logger.debug("Table name: ${jdbcOptions.tableName}")
182183
logger.debug("SQL query: ${jdbcOptions.sqlQuery}")
183184

184-
return if (jdbcOptions.tableName.isNotBlank()) {
185-
DataFrame.getSchemaForSqlTable(connection, jdbcOptions.tableName)
186-
} else if (jdbcOptions.sqlQuery.isNotBlank()) {
187-
DataFrame.getSchemaForSqlQuery(connection, jdbcOptions.sqlQuery)
188-
} else {
189-
throw RuntimeException(
190-
"Table name: ${jdbcOptions.tableName}, " +
191-
"SQL query: ${jdbcOptions.sqlQuery} both are empty! " +
192-
"Populate 'tableName' or 'sqlQuery' in jdbcOptions with value to generate schema " +
193-
"for SQL table or result of SQL query!"
194-
)
185+
val tableName = jdbcOptions.tableName
186+
val sqlQuery = jdbcOptions.sqlQuery
187+
188+
return when {
189+
isTableNameNotBlankAndQueryBlank(tableName, sqlQuery) -> generateSchemaForTable(connection, tableName)
190+
isQueryNotBlankAndTableBlank(tableName, sqlQuery) -> generateSchemaForQuery(connection, sqlQuery)
191+
areBothNotBlank(tableName, sqlQuery) -> throwBothFieldsFilledException(tableName, sqlQuery)
192+
else -> throwBothFieldsEmptyException(tableName, sqlQuery)
195193
}
196194
}
197195

196+
private fun isTableNameNotBlankAndQueryBlank(tableName: String, sqlQuery: String) =
197+
tableName.isNotBlank() && sqlQuery.isBlank()
198+
199+
private fun isQueryNotBlankAndTableBlank(tableName: String, sqlQuery: String) =
200+
sqlQuery.isNotBlank() && tableName.isBlank()
201+
202+
private fun areBothNotBlank(tableName: String, sqlQuery: String) = sqlQuery.isNotBlank() && tableName.isNotBlank()
203+
204+
private fun generateSchemaForTable(connection: Connection, tableName: String) =
205+
DataFrame.getSchemaForSqlTable(connection, tableName)
206+
207+
private fun generateSchemaForQuery(connection: Connection, sqlQuery: String) =
208+
DataFrame.getSchemaForSqlQuery(connection, sqlQuery)
209+
210+
private fun throwBothFieldsFilledException(tableName: String, sqlQuery: String): Nothing {
211+
throw RuntimeException(
212+
"Table name '$tableName' and SQL query '$sqlQuery' both are filled! " +
213+
"Clear 'tableName' or 'sqlQuery' properties in jdbcOptions with value to generate schema for SQL table or result of SQL query!"
214+
)
215+
}
216+
217+
private fun throwBothFieldsEmptyException(tableName: String, sqlQuery: String): Nothing {
218+
throw RuntimeException(
219+
"Table name '$tableName' and SQL query '$sqlQuery' both are empty! " +
220+
"Populate 'tableName' or 'sqlQuery' properties in jdbcOptions with value to generate schema for SQL table or result of SQL query!"
221+
)
222+
}
223+
198224
private fun stringOf(data: Any): String =
199225
when (data) {
200226
is File -> data.absolutePath

plugins/dataframe-gradle-plugin/src/main/kotlin/org/jetbrains/dataframe/gradle/SchemaGeneratorExtension.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,17 @@ data class JsonOptionsDsl(
132132
var keyValuePaths: List<JsonPath> = emptyList(),
133133
) : Serializable
134134

135+
/**
136+
* Represents the configuration options for JDBC data source.
137+
*
138+
* @property [user] The username used to authenticate with the database. Default is an empty string.
139+
* @property [password] The password used to authenticate with the database. Default is an empty string.
140+
* @property [tableName] The name of the table to generate schema for. Default is an empty string.
141+
* @property [sqlQuery] The SQL query used to generate schema. Default is an empty string.
142+
*/
135143
data class JdbcOptionsDsl(
136-
var user: String = "", // TODO: I'm not sure about the default parameters
137-
var password: String = "", // TODO: I'm not sure about the default parameters
144+
var user: String = "",
145+
var password: String = "",
138146
var tableName: String = "",
139147
var sqlQuery: String = ""
140148
) : Serializable

plugins/dataframe-gradle-plugin/src/main/kotlin/org/jetbrains/dataframe/gradle/SchemaGeneratorPlugin.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ class SchemaGeneratorPlugin : Plugin<Project> {
132132
this.schemaVisibility.set(visibility)
133133
this.csvOptions.set(schema.csvOptions)
134134
this.jsonOptions.set(schema.jsonOptions)
135-
this.jdbcOptions.set(schema.jdbcOptions) // TODO: probably remove
135+
this.jdbcOptions.set(schema.jdbcOptions)
136136
this.defaultPath.set(defaultPath)
137137
this.delimiters.set(delimiters)
138138
}

plugins/symbol-processor/src/main/kotlin/org/jetbrains/dataframe/ksp/DataSchemaGenerator.kt

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,19 @@ class DataSchemaGenerator(
172172
// Force classloading
173173
Class.forName(driverClassNameFromUrl(url))
174174

175+
var userName = importStatement.jdbcOptions.user
176+
var password = importStatement.jdbcOptions.password
177+
178+
// treat the passed userName and password parameters as env variables
179+
if (importStatement.jdbcOptions.extractCredFromEnv) {
180+
userName = System.getenv(userName) ?: userName
181+
password = System.getenv(password) ?: password
182+
}
183+
175184
val connection = DriverManager.getConnection(
176185
url,
177-
importStatement.jdbcOptions.user,
178-
importStatement.jdbcOptions.password
186+
userName,
187+
password
179188
)
180189

181190
connection.use {
@@ -271,22 +280,47 @@ class DataSchemaGenerator(
271280

272281
private fun generateSchemaForImport(
273282
importStatement: ImportDataSchemaStatement,
274-
connection: Connection,
283+
connection: Connection
275284
): DataFrameSchema {
276285
logger.info("Table name: ${importStatement.jdbcOptions.tableName}")
277286
logger.info("SQL query: ${importStatement.jdbcOptions.sqlQuery}")
278287

279-
return if (importStatement.jdbcOptions.tableName.isNotBlank()) {
280-
DataFrame.getSchemaForSqlTable(connection, importStatement.jdbcOptions.tableName)
281-
} else if (importStatement.jdbcOptions.sqlQuery.isNotBlank()) {
282-
DataFrame.getSchemaForSqlQuery(connection, importStatement.jdbcOptions.sqlQuery)
283-
} else {
284-
throw RuntimeException(
285-
"Table name: ${importStatement.jdbcOptions.tableName}, " +
286-
"SQL query: ${importStatement.jdbcOptions.sqlQuery} both are empty! " +
287-
"Populate 'tableName' or 'sqlQuery' in jdbcOptions with value to generate schema " +
288-
"for SQL table or result of SQL query!"
289-
)
288+
val tableName = importStatement.jdbcOptions.tableName
289+
val sqlQuery = importStatement.jdbcOptions.sqlQuery
290+
291+
return when {
292+
isTableNameNotBlankAndQueryBlank(tableName, sqlQuery) -> generateSchemaForTable(connection, tableName)
293+
isQueryNotBlankAndTableBlank(tableName, sqlQuery) -> generateSchemaForQuery(connection, sqlQuery)
294+
areBothNotBlank(tableName, sqlQuery) -> throwBothFieldsFilledException(tableName, sqlQuery)
295+
else -> throwBothFieldsEmptyException(tableName, sqlQuery)
290296
}
291297
}
298+
299+
private fun isTableNameNotBlankAndQueryBlank(tableName: String, sqlQuery: String) =
300+
tableName.isNotBlank() && sqlQuery.isBlank()
301+
302+
private fun isQueryNotBlankAndTableBlank(tableName: String, sqlQuery: String) =
303+
sqlQuery.isNotBlank() && tableName.isBlank()
304+
305+
private fun areBothNotBlank(tableName: String, sqlQuery: String) = sqlQuery.isNotBlank() && tableName.isNotBlank()
306+
307+
private fun generateSchemaForTable(connection: Connection, tableName: String) =
308+
DataFrame.getSchemaForSqlTable(connection, tableName)
309+
310+
private fun generateSchemaForQuery(connection: Connection, sqlQuery: String) =
311+
DataFrame.getSchemaForSqlQuery(connection, sqlQuery)
312+
313+
private fun throwBothFieldsFilledException(tableName: String, sqlQuery: String): Nothing {
314+
throw RuntimeException(
315+
"Table name '$tableName' and SQL query '$sqlQuery' both are filled! " +
316+
"Clear 'tableName' or 'sqlQuery' properties in jdbcOptions with value to generate schema for SQL table or result of SQL query!"
317+
)
318+
}
319+
320+
private fun throwBothFieldsEmptyException(tableName: String, sqlQuery: String): Nothing {
321+
throw RuntimeException(
322+
"Table name '$tableName' and SQL query '$sqlQuery' both are empty! " +
323+
"Populate 'tableName' or 'sqlQuery' properties in jdbcOptions with value to generate schema for SQL table or result of SQL query!"
324+
)
325+
}
292326
}

plugins/symbol-processor/src/test/kotlin/org/jetbrains/dataframe/ksp/DataFrameJdbcSymbolProcessorTest.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,62 @@ class DataFrameJdbcSymbolProcessorTest {
212212
result.successfulCompilation shouldBe true
213213
}
214214

215+
/**
216+
* Test code is copied from test above.
217+
*/
218+
@Test
219+
fun `schema extracted via readFromDB method is resolved with db credentials from env variables`() {
220+
val result = KspCompilationTestRunner.compile(
221+
TestCompilationParameters(
222+
sources = listOf(
223+
SourceFile.kotlin(
224+
"MySources.kt",
225+
"""
226+
@file:ImportDataSchema(
227+
"Customer",
228+
"$CONNECTION_URL",
229+
jdbcOptions = JdbcOptions("", "", extractCredFromEnv = true, tableName = "Customer")
230+
)
231+
232+
package test
233+
234+
import org.jetbrains.kotlinx.dataframe.annotations.ImportDataSchema
235+
import org.jetbrains.kotlinx.dataframe.annotations.JdbcOptions
236+
import org.jetbrains.kotlinx.dataframe.api.filter
237+
import org.jetbrains.kotlinx.dataframe.DataFrame
238+
import org.jetbrains.kotlinx.dataframe.api.cast
239+
import java.sql.Connection
240+
import java.sql.DriverManager
241+
import java.sql.SQLException
242+
import org.jetbrains.kotlinx.dataframe.io.readSqlTable
243+
import org.jetbrains.kotlinx.dataframe.io.DatabaseConfiguration
244+
245+
fun main() {
246+
val tableName = "Customer"
247+
DriverManager.getConnection("$CONNECTION_URL").use { connection ->
248+
val df = DataFrame.readSqlTable(connection, tableName).cast<Customer>()
249+
df.filter { it[Customer::age] != null && it[Customer::age]!! > 30 }
250+
251+
val df1 = DataFrame.readSqlTable(connection, tableName, 1).cast<Customer>()
252+
df1.filter { it[Customer::age] != null && it[Customer::age]!! > 30 }
253+
254+
val dbConfig = DatabaseConfiguration(url = "$CONNECTION_URL")
255+
val df2 = DataFrame.readSqlTable(dbConfig, tableName).cast<Customer>()
256+
df2.filter { it[Customer::age] != null && it[Customer::age]!! > 30 }
257+
258+
val df3 = DataFrame.readSqlTable(dbConfig, tableName, 1).cast<Customer>()
259+
df3.filter { it[Customer::age] != null && it[Customer::age]!! > 30 }
260+
261+
}
262+
}
263+
""".trimIndent()
264+
)
265+
)
266+
)
267+
)
268+
result.successfulCompilation shouldBe true
269+
}
270+
215271
private fun KotlinCompileTestingCompilationResult.inspectLines(f: (List<String>) -> Unit) {
216272
inspectLines(generatedFile, f)
217273
}

0 commit comments

Comments
 (0)