@@ -22,6 +22,7 @@ import java.sql.Ref
22
22
import java.sql.ResultSet
23
23
import java.sql.ResultSetMetaData
24
24
import java.sql.RowId
25
+ import java.sql.SQLException
25
26
import java.sql.SQLXML
26
27
import java.sql.Time
27
28
import java.sql.Timestamp
@@ -104,13 +105,105 @@ public data class TableColumnMetadata(
104
105
public data class TableMetadata (val name : String , val schemaName : String? , val catalogue : String? )
105
106
106
107
/* *
107
- * Represents the configuration for a database connection.
108
+ * Represents the configuration for an internally managed JDBC database connection.
108
109
*
109
- * @property [url] the URL of the database. Keep it in the following form jdbc:subprotocol:subnam
110
- * @property [user] the username used for authentication (optional, default is empty string).
111
- * @property [password] the password used for authentication (optional, default is empty string).
110
+ * This class defines connection parameters used by the library to create a `Connection`
111
+ * when the user does not provide one explicitly. It is designed for safe, read-only access by default.
112
+ *
113
+ * @property url The JDBC URL of the database, e.g., `"jdbc:postgresql://localhost:5432/mydb"`.
114
+ * Must follow the standard format: `jdbc:subprotocol:subname`.
115
+ *
116
+ * @property user The username used for authentication.
117
+ * Optional, default is an empty string.
118
+ *
119
+ * @property password The password used for authentication.
120
+ * Optional, default is an empty string.
121
+ *
122
+ * @property readOnly If `true` (default), the library will create the connection in read-only mode.
123
+ * This enables the following behavior:
124
+ * - `Connection.setReadOnly(true)`
125
+ * - `Connection.setAutoCommit(false)`
126
+ * - automatic `rollback()` at the end of execution
127
+ *
128
+ * If `false`, the connection will be created with JDBC defaults (usually read-write),
129
+ * but the library will still reject any queries that appear to modify data
130
+ * (e.g. contain `INSERT`, `UPDATE`, `DELETE`, etc.).
131
+ *
132
+ * Note: Connections created using this configuration are managed entirely by the library.
133
+ * Users do not have access to the underlying `Connection` instance and cannot commit or close it manually.
134
+ *
135
+ * ### Examples:
136
+ *
137
+ * ```kotlin
138
+ * // Safe read-only connection (default)
139
+ * val config = DbConnectionConfig("jdbc:sqlite::memory:")
140
+ * val df = DataFrame.readSqlQuery(config, "SELECT * FROM books")
141
+ *
142
+ * // Use default JDBC connection settings (still protected against mutations)
143
+ * val config = DbConnectionConfig(
144
+ * url = "jdbc:sqlite::memory:",
145
+ * readOnly = false
146
+ * )
147
+ * ```
112
148
*/
113
- public data class DbConnectionConfig (val url : String , val user : String = " " , val password : String = " " )
149
+ public data class DbConnectionConfig (
150
+ val url : String ,
151
+ val user : String = " " ,
152
+ val password : String = " " ,
153
+ val readOnly : Boolean = true ,
154
+ )
155
+
156
+ /* *
157
+ * Executes the given block with a managed JDBC connection created from [DbConnectionConfig].
158
+ *
159
+ * If [DbConnectionConfig.readOnly] is `true` (default), the connection will be:
160
+ * - explicitly marked as read-only
161
+ * - used with auto-commit disabled
162
+ * - rolled back after execution to prevent unintended modifications
163
+ *
164
+ * This utility guarantees proper closing of the connection and safe rollback in read-only mode.
165
+ * It should be used when the user does not manually manage JDBC connections.
166
+ *
167
+ * @param [dbConfig] The configuration used to create the connection.
168
+ * @param [dbType] Optional database type (not used here but can be passed through for logging or future extensions).
169
+ * @param [block] A lambda with receiver that runs with an open and managed [Connection].
170
+ * @return The result of the [block] execution.
171
+ */
172
+ internal inline fun <T > withReadOnlyConnection (
173
+ dbConfig : DbConnectionConfig ,
174
+ dbType : DbType ? = null,
175
+ block : (Connection ) -> T ,
176
+ ): T {
177
+ val connection = DriverManager .getConnection(dbConfig.url, dbConfig.user, dbConfig.password)
178
+
179
+ val originalAutoCommit = connection.autoCommit
180
+ val originalReadOnly = connection.isReadOnly
181
+
182
+ return connection.use { conn ->
183
+ try {
184
+ if (dbConfig.readOnly) {
185
+ conn.autoCommit = false
186
+ conn.isReadOnly = true
187
+ }
188
+
189
+ block(conn)
190
+ } finally {
191
+ if (dbConfig.readOnly) {
192
+ try {
193
+ conn.rollback()
194
+ } catch (e: SQLException ) {
195
+ logger.warn(e) {
196
+ " Failed to rollback read-only transaction (url=${dbConfig.url} )"
197
+ }
198
+ }
199
+ }
200
+
201
+ // Restore original settings (relevant in pooled environments)
202
+ conn.autoCommit = originalAutoCommit
203
+ conn.isReadOnly = originalReadOnly
204
+ }
205
+ }
206
+ }
114
207
115
208
/* *
116
209
* Reads data from an SQL table and converts it into a DataFrame.
@@ -124,6 +217,15 @@ public data class DbConnectionConfig(val url: String, val user: String = "", val
124
217
* @param [strictValidation] if `true`, the method validates that the provided table name is in a valid format.
125
218
* Default is `true` for strict validation.
126
219
* @return the DataFrame containing the data from the SQL table.
220
+ *
221
+ * ### Default Behavior:
222
+ * If [DbConnectionConfig.readOnly] is `true` (which is the default), the connection will be:
223
+ * - explicitly set as read-only via `Connection.setReadOnly(true)`
224
+ * - used with `autoCommit = false`
225
+ * - automatically rolled back after reading, ensuring no changes to the database
226
+ *
227
+ * Even if [DbConnectionConfig.readOnly] is set to `false`, the library still prevents data-modifying queries
228
+ * and only permits safe `SELECT` operations internally.
127
229
*/
128
230
public fun DataFrame.Companion.readSqlTable (
129
231
dbConfig : DbConnectionConfig ,
@@ -132,11 +234,10 @@ public fun DataFrame.Companion.readSqlTable(
132
234
inferNullability : Boolean = true,
133
235
dbType : DbType ? = null,
134
236
strictValidation : Boolean = true,
135
- ): AnyFrame {
136
- DriverManager .getConnection (dbConfig.url, dbConfig.user, dbConfig.password).use { connection ->
137
- return readSqlTable(connection , tableName, limit, inferNullability, dbType, strictValidation)
237
+ ): AnyFrame =
238
+ withReadOnlyConnection (dbConfig, dbType) { conn ->
239
+ readSqlTable(conn , tableName, limit, inferNullability, dbType, strictValidation)
138
240
}
139
- }
140
241
141
242
/* *
142
243
* Reads data from an SQL table and converts it into a DataFrame.
@@ -203,6 +304,15 @@ public fun DataFrame.Companion.readSqlTable(
203
304
* @param [strictValidation] if `true`, the method validates that the provided query is in a valid format.
204
305
* Default is `true` for strict validation.
205
306
* @return the DataFrame containing the result of the SQL query.
307
+ *
308
+ * ### Default Behavior:
309
+ * If [DbConnectionConfig.readOnly] is `true` (which is the default), the connection will be:
310
+ * - explicitly set as read-only via `Connection.setReadOnly(true)`
311
+ * - used with `autoCommit = false`
312
+ * - automatically rolled back after reading, ensuring no changes to the database
313
+ *
314
+ * Even if [DbConnectionConfig.readOnly] is set to `false`, the library still prevents data-modifying queries
315
+ * and only permits safe `SELECT` operations internally.
206
316
*/
207
317
208
318
public fun DataFrame.Companion.readSqlQuery (
@@ -212,11 +322,10 @@ public fun DataFrame.Companion.readSqlQuery(
212
322
inferNullability : Boolean = true,
213
323
dbType : DbType ? = null,
214
324
strictValidation : Boolean = true,
215
- ): AnyFrame {
216
- DriverManager .getConnection (dbConfig.url, dbConfig.user, dbConfig.password).use { connection ->
217
- return readSqlQuery(connection , sqlQuery, limit, inferNullability, dbType, strictValidation)
325
+ ): AnyFrame =
326
+ withReadOnlyConnection (dbConfig, dbType) { conn ->
327
+ readSqlQuery(conn , sqlQuery, limit, inferNullability, dbType, strictValidation)
218
328
}
219
- }
220
329
221
330
/* *
222
331
* Converts the result of an SQL query to the DataFrame.
@@ -281,6 +390,15 @@ public fun DataFrame.Companion.readSqlQuery(
281
390
* @param [strictValidation] if `true`, the method validates that the provided query or table name is in a valid format.
282
391
* Default is `true` for strict validation.
283
392
* @return the DataFrame containing the result of the SQL query.
393
+ *
394
+ * ### Default Behavior:
395
+ * If [DbConnectionConfig.readOnly] is `true` (which is the default), the connection will be:
396
+ * - explicitly set as read-only via `Connection.setReadOnly(true)`
397
+ * - used with `autoCommit = false`
398
+ * - automatically rolled back after reading, ensuring no changes to the database
399
+ *
400
+ * Even if [DbConnectionConfig.readOnly] is set to `false`, the library still prevents data-modifying queries
401
+ * and only permits safe `SELECT` operations internally.
284
402
*/
285
403
public fun DbConnectionConfig.readDataFrame (
286
404
sqlQueryOrTableName : String ,
@@ -638,18 +756,26 @@ public fun ResultSet.readDataFrame(
638
756
* @param [dbType] the type of database, could be a custom object, provided by user, optional, default is `null`,
639
757
* in that case the [dbType] will be recognized from the [dbConfig].
640
758
* @return a map of [String] to [AnyFrame] objects representing the non-system tables from the database.
759
+ *
760
+ * ### Default Behavior:
761
+ * If [DbConnectionConfig.readOnly] is `true` (which is the default), the connection will be:
762
+ * - explicitly set as read-only via `Connection.setReadOnly(true)`
763
+ * - used with `autoCommit = false`
764
+ * - automatically rolled back after reading, ensuring no changes to the database
765
+ *
766
+ * Even if [DbConnectionConfig.readOnly] is set to `false`, the library still prevents data-modifying queries
767
+ * and only permits safe `SELECT` operations internally.
641
768
*/
642
769
public fun DataFrame.Companion.readAllSqlTables (
643
770
dbConfig : DbConnectionConfig ,
644
771
catalogue : String? = null,
645
772
limit : Int = DEFAULT_LIMIT ,
646
773
inferNullability : Boolean = true,
647
774
dbType : DbType ? = null,
648
- ): Map <String , AnyFrame > {
649
- DriverManager .getConnection (dbConfig.url, dbConfig.user, dbConfig.password).use { connection ->
650
- return readAllSqlTables(connection, catalogue, limit, inferNullability, dbType)
775
+ ): Map <String , AnyFrame > =
776
+ withReadOnlyConnection (dbConfig, dbType) { connection ->
777
+ readAllSqlTables(connection, catalogue, limit, inferNullability, dbType)
651
778
}
652
- }
653
779
654
780
/* *
655
781
* Reads all non-system tables from a database and returns them
@@ -712,16 +838,24 @@ public fun DataFrame.Companion.readAllSqlTables(
712
838
* @param [dbType] the type of database, could be a custom object, provided by user, optional, default is `null`,
713
839
* in that case the [dbType] will be recognized from the [dbConfig].
714
840
* @return the [DataFrameSchema] object representing the schema of the SQL table
841
+ *
842
+ * ### Default Behavior:
843
+ * If [DbConnectionConfig.readOnly] is `true` (which is the default), the connection will be:
844
+ * - explicitly set as read-only via `Connection.setReadOnly(true)`
845
+ * - used with `autoCommit = false`
846
+ * - automatically rolled back after reading, ensuring no changes to the database
847
+ *
848
+ * Even if [DbConnectionConfig.readOnly] is set to `false`, the library still prevents data-modifying queries
849
+ * and only permits safe `SELECT` operations internally.
715
850
*/
716
851
public fun DataFrame.Companion.getSchemaForSqlTable (
717
852
dbConfig : DbConnectionConfig ,
718
853
tableName : String ,
719
854
dbType : DbType ? = null,
720
- ): DataFrameSchema {
721
- DriverManager .getConnection (dbConfig.url, dbConfig.user, dbConfig.password).use { connection ->
722
- return getSchemaForSqlTable(connection, tableName, dbType)
855
+ ): DataFrameSchema =
856
+ withReadOnlyConnection (dbConfig, dbType) { connection ->
857
+ getSchemaForSqlTable(connection, tableName, dbType)
723
858
}
724
- }
725
859
726
860
/* *
727
861
* Retrieves the schema for an SQL table using the provided database connection.
@@ -760,16 +894,24 @@ public fun DataFrame.Companion.getSchemaForSqlTable(
760
894
* @param [dbType] the type of database, could be a custom object, provided by user, optional, default is `null`,
761
895
* in that case the [dbType] will be recognized from the [dbConfig].
762
896
* @return the schema of the SQL query as a [DataFrameSchema] object.
897
+ *
898
+ * ### Default Behavior:
899
+ * If [DbConnectionConfig.readOnly] is `true` (which is the default), the connection will be:
900
+ * - explicitly set as read-only via `Connection.setReadOnly(true)`
901
+ * - used with `autoCommit = false`
902
+ * - automatically rolled back after reading, ensuring no changes to the database
903
+ *
904
+ * Even if [DbConnectionConfig.readOnly] is set to `false`, the library still prevents data-modifying queries
905
+ * and only permits safe `SELECT` operations internally.
763
906
*/
764
907
public fun DataFrame.Companion.getSchemaForSqlQuery (
765
908
dbConfig : DbConnectionConfig ,
766
909
sqlQuery : String ,
767
910
dbType : DbType ? = null,
768
- ): DataFrameSchema {
769
- DriverManager .getConnection (dbConfig.url, dbConfig.user, dbConfig.password).use { connection ->
770
- return getSchemaForSqlQuery(connection, sqlQuery, dbType)
911
+ ): DataFrameSchema =
912
+ withReadOnlyConnection (dbConfig, dbType) { connection ->
913
+ getSchemaForSqlQuery(connection, sqlQuery, dbType)
771
914
}
772
- }
773
915
774
916
/* *
775
917
* Retrieves the schema of an SQL query result using the provided database connection.
@@ -804,6 +946,15 @@ public fun DataFrame.Companion.getSchemaForSqlQuery(
804
946
* @param [dbType] the type of database, could be a custom object, provided by user, optional, default is `null`,
805
947
* in that case the [dbType] will be recognized from the [DbConnectionConfig].
806
948
* @return the schema of the SQL query as a [DataFrameSchema] object.
949
+ *
950
+ * ### Default Behavior:
951
+ * If [DbConnectionConfig.readOnly] is `true` (which is the default), the connection will be:
952
+ * - explicitly set as read-only via `Connection.setReadOnly(true)`
953
+ * - used with `autoCommit = false`
954
+ * - automatically rolled back after reading, ensuring no changes to the database
955
+ *
956
+ * Even if [DbConnectionConfig.readOnly] is set to `false`, the library still prevents data-modifying queries
957
+ * and only permits safe `SELECT` operations internally.
807
958
*/
808
959
public fun DbConnectionConfig.getDataFrameSchema (
809
960
sqlQueryOrTableName : String ,
@@ -869,15 +1020,23 @@ public fun ResultSet.getDataFrameSchema(dbType: DbType): DataFrameSchema = DataF
869
1020
* @param [dbType] the type of database, could be a custom object, provided by user, optional, default is `null`,
870
1021
* in that case the [dbType] will be recognized from the [dbConfig].
871
1022
* @return a map of [String, DataFrameSchema] objects representing the table name and its schema for each non-system table.
1023
+ *
1024
+ * ### Default Behavior:
1025
+ * If [DbConnectionConfig.readOnly] is `true` (which is the default), the connection will be:
1026
+ * - explicitly set as read-only via `Connection.setReadOnly(true)`
1027
+ * - used with `autoCommit = false`
1028
+ * - automatically rolled back after reading, ensuring no changes to the database
1029
+ *
1030
+ * Even if [DbConnectionConfig.readOnly] is set to `false`, the library still prevents data-modifying queries
1031
+ * and only permits safe `SELECT` operations internally.
872
1032
*/
873
1033
public fun DataFrame.Companion.getSchemaForAllSqlTables (
874
1034
dbConfig : DbConnectionConfig ,
875
1035
dbType : DbType ? = null,
876
- ): Map <String , DataFrameSchema > {
877
- DriverManager .getConnection (dbConfig.url, dbConfig.user, dbConfig.password).use { connection ->
878
- return getSchemaForAllSqlTables(connection, dbType)
1036
+ ): Map <String , DataFrameSchema > =
1037
+ withReadOnlyConnection (dbConfig, dbType) { connection ->
1038
+ getSchemaForAllSqlTables(connection, dbType)
879
1039
}
880
- }
881
1040
882
1041
/* *
883
1042
* Retrieves the schemas of all non-system tables in the database using the provided database connection.
0 commit comments