@@ -22,6 +22,7 @@ import java.sql.Ref
2222import java.sql.ResultSet
2323import java.sql.ResultSetMetaData
2424import java.sql.RowId
25+ import java.sql.SQLException
2526import java.sql.SQLXML
2627import java.sql.Time
2728import java.sql.Timestamp
@@ -104,13 +105,105 @@ public data class TableColumnMetadata(
104105public data class TableMetadata (val name : String , val schemaName : String? , val catalogue : String? )
105106
106107/* *
107- * Represents the configuration for a database connection.
108+ * Represents the configuration for an internally managed JDBC database connection.
108109 *
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+ * ```
112148 */
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+ }
114207
115208/* *
116209 * 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
124217 * @param [strictValidation] if `true`, the method validates that the provided table name is in a valid format.
125218 * Default is `true` for strict validation.
126219 * @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.
127229 */
128230public fun DataFrame.Companion.readSqlTable (
129231 dbConfig : DbConnectionConfig ,
@@ -132,11 +234,10 @@ public fun DataFrame.Companion.readSqlTable(
132234 inferNullability : Boolean = true,
133235 dbType : DbType ? = null,
134236 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)
138240 }
139- }
140241
141242/* *
142243 * Reads data from an SQL table and converts it into a DataFrame.
@@ -203,6 +304,15 @@ public fun DataFrame.Companion.readSqlTable(
203304 * @param [strictValidation] if `true`, the method validates that the provided query is in a valid format.
204305 * Default is `true` for strict validation.
205306 * @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.
206316 */
207317
208318public fun DataFrame.Companion.readSqlQuery (
@@ -212,11 +322,10 @@ public fun DataFrame.Companion.readSqlQuery(
212322 inferNullability : Boolean = true,
213323 dbType : DbType ? = null,
214324 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)
218328 }
219- }
220329
221330/* *
222331 * Converts the result of an SQL query to the DataFrame.
@@ -281,6 +390,15 @@ public fun DataFrame.Companion.readSqlQuery(
281390 * @param [strictValidation] if `true`, the method validates that the provided query or table name is in a valid format.
282391 * Default is `true` for strict validation.
283392 * @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.
284402 */
285403public fun DbConnectionConfig.readDataFrame (
286404 sqlQueryOrTableName : String ,
@@ -638,18 +756,26 @@ public fun ResultSet.readDataFrame(
638756 * @param [dbType] the type of database, could be a custom object, provided by user, optional, default is `null`,
639757 * in that case the [dbType] will be recognized from the [dbConfig].
640758 * @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.
641768 */
642769public fun DataFrame.Companion.readAllSqlTables (
643770 dbConfig : DbConnectionConfig ,
644771 catalogue : String? = null,
645772 limit : Int = DEFAULT_LIMIT ,
646773 inferNullability : Boolean = true,
647774 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)
651778 }
652- }
653779
654780/* *
655781 * Reads all non-system tables from a database and returns them
@@ -712,16 +838,24 @@ public fun DataFrame.Companion.readAllSqlTables(
712838 * @param [dbType] the type of database, could be a custom object, provided by user, optional, default is `null`,
713839 * in that case the [dbType] will be recognized from the [dbConfig].
714840 * @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.
715850 */
716851public fun DataFrame.Companion.getSchemaForSqlTable (
717852 dbConfig : DbConnectionConfig ,
718853 tableName : String ,
719854 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)
723858 }
724- }
725859
726860/* *
727861 * Retrieves the schema for an SQL table using the provided database connection.
@@ -760,16 +894,24 @@ public fun DataFrame.Companion.getSchemaForSqlTable(
760894 * @param [dbType] the type of database, could be a custom object, provided by user, optional, default is `null`,
761895 * in that case the [dbType] will be recognized from the [dbConfig].
762896 * @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.
763906 */
764907public fun DataFrame.Companion.getSchemaForSqlQuery (
765908 dbConfig : DbConnectionConfig ,
766909 sqlQuery : String ,
767910 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)
771914 }
772- }
773915
774916/* *
775917 * Retrieves the schema of an SQL query result using the provided database connection.
@@ -804,6 +946,15 @@ public fun DataFrame.Companion.getSchemaForSqlQuery(
804946 * @param [dbType] the type of database, could be a custom object, provided by user, optional, default is `null`,
805947 * in that case the [dbType] will be recognized from the [DbConnectionConfig].
806948 * @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.
807958 */
808959public fun DbConnectionConfig.getDataFrameSchema (
809960 sqlQueryOrTableName : String ,
@@ -869,15 +1020,23 @@ public fun ResultSet.getDataFrameSchema(dbType: DbType): DataFrameSchema = DataF
8691020 * @param [dbType] the type of database, could be a custom object, provided by user, optional, default is `null`,
8701021 * in that case the [dbType] will be recognized from the [dbConfig].
8711022 * @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.
8721032 */
8731033public fun DataFrame.Companion.getSchemaForAllSqlTables (
8741034 dbConfig : DbConnectionConfig ,
8751035 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)
8791039 }
880- }
8811040
8821041/* *
8831042 * Retrieves the schemas of all non-system tables in the database using the provided database connection.
0 commit comments