This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch 15501-enhance-datasource-properties-tests
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit ea92589189d39b43cf0d911f8422235fd0fbaa73
Author: James Fredley <[email protected]>
AuthorDate: Thu Mar 12 20:39:30 2026 -0400

    Generalize Map-to-Properties coercion and add comprehensive HikariCP 
binding tests
    
    Refactor coerceDbProperties() into a generic coerceMapToProperties(key) 
method
    that handles dataSourceProperties and healthCheckProperties in addition to
    dbProperties. Without this, using the dataSourceProperties key directly in
    application.yml silently drops JDBC driver pass-through properties because 
the
    Map-to-Properties coercion never runs.
    
    Add tests covering all HikariCP pool properties configurable through
    DataSourceBuilder: pool sizing, timeouts, connection behavior, pool 
management,
    driver pass-through properties, and health check properties.
    
    Assisted-by: Claude Code <[email protected]>
---
 .../datastore/gorm/jdbc/DataSourceBuilder.java     |  34 +--
 .../gorm/jdbc/DataSourceBuilderSpec.groovy         | 260 +++++++++++++++++++++
 2 files changed, 281 insertions(+), 13 deletions(-)

diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/DataSourceBuilder.java
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/DataSourceBuilder.java
index c69af9576e..b9693acad4 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/DataSourceBuilder.java
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/jdbc/DataSourceBuilder.java
@@ -98,9 +98,9 @@ public class DataSourceBuilder {
     }
 
     private void bind(DataSource result) {
-        if (properties.containsKey("dbProperties")) {
-            coerceDbProperties();
-        }
+        coerceMapToProperties("dbProperties");
+        coerceMapToProperties("dataSourceProperties");
+        coerceMapToProperties("healthCheckProperties");
         MutablePropertyValues properties = new 
MutablePropertyValues(this.properties);
         new RelaxedDataBinder(result).withAlias("url", "jdbcUrl")
                 .withAlias("username", "user")
@@ -114,19 +114,27 @@ public class DataSourceBuilder {
         return this;
     }
 
-    private void coerceDbProperties() {
+    /**
+     * Coerce a Map value for the given key into a {@link Properties} instance.
+     * This is needed because YAML configuration produces {@link 
java.util.LinkedHashMap}
+     * instances, but HikariConfig setters like {@code 
setDataSourceProperties} and
+     * {@code setHealthCheckProperties} require {@link Properties} objects.
+     *
+     * @param key the property key whose Map value should be coerced to 
Properties
+     */
+    private void coerceMapToProperties(String key) {
         Map propertiesMap = this.properties;
-        Object dbPropertiesObject = propertiesMap.get("dbProperties");
-        if (dbPropertiesObject instanceof Map) {
-            Map dbProperties = (Map) dbPropertiesObject;
-            Properties properties = new Properties();
-            for (Object key : dbProperties.keySet()) {
-                Object value = dbProperties.get(key);
-                if (value != null) {
-                    properties.put(key.toString(), value.toString());
+        Object value = propertiesMap.get(key);
+        if (value instanceof Map && !(value instanceof Properties)) {
+            Map map = (Map) value;
+            Properties coerced = new Properties();
+            for (Object k : map.keySet()) {
+                Object v = map.get(k);
+                if (v != null) {
+                    coerced.put(k.toString(), v.toString());
                 }
             }
-            propertiesMap.put("dbProperties", properties);
+            propertiesMap.put(key, coerced);
         }
     }
 
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/DataSourceBuilderSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/DataSourceBuilderSpec.groovy
index 7ca50fb12b..d005b59f60 100644
--- 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/DataSourceBuilderSpec.groovy
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/DataSourceBuilderSpec.groovy
@@ -256,4 +256,264 @@ class DataSourceBuilderSpec extends Specification {
         cleanup:
         ds?.close()
     }
+
+    def "dataSourceProperties key is coerced from Map and bound directly to 
HikariDataSource"() {
+        given:
+        def builder = DataSourceBuilder.create()
+                .type(HikariDataSource)
+                .url("jdbc:h2:mem:directDsPropsTest;DB_CLOSE_DELAY=-1")
+                .driverClassName("org.h2.Driver")
+                .username("sa")
+                .password("")
+
+        Map<String, String> props = [
+                'dataSourceProperties': [cachePrepStmts: 'true', 
prepStmtCacheSize: '250']
+        ]
+        builder.properties(props)
+
+        when:
+        HikariDataSource ds = (HikariDataSource) builder.build()
+
+        then:
+        ds.dataSourceProperties != null
+        ds.dataSourceProperties.getProperty('cachePrepStmts') == 'true'
+        ds.dataSourceProperties.getProperty('prepStmtCacheSize') == '250'
+
+        cleanup:
+        ds?.close()
+    }
+
+    def "coercion skips values already of type Properties"() {
+        given:
+        def builder = DataSourceBuilder.create()
+                .type(HikariDataSource)
+                .url("jdbc:h2:mem:alreadyPropsTest;DB_CLOSE_DELAY=-1")
+                .driverClassName("org.h2.Driver")
+                .username("sa")
+                .password("")
+
+        Properties existingProps = new Properties()
+        existingProps.setProperty('cachePrepStmts', 'true')
+
+        Map props = [dataSourceProperties: existingProps]
+        builder.properties(props)
+
+        when:
+        HikariDataSource ds = (HikariDataSource) builder.build()
+
+        then:
+        ds.dataSourceProperties.getProperty('cachePrepStmts') == 'true'
+
+        cleanup:
+        ds?.close()
+    }
+
+    def "healthCheckProperties as Map is coerced to Properties and bound to 
HikariDataSource"() {
+        given:
+        def builder = DataSourceBuilder.create()
+                .type(HikariDataSource)
+                .url("jdbc:h2:mem:healthCheckTest;DB_CLOSE_DELAY=-1")
+                .driverClassName("org.h2.Driver")
+                .username("sa")
+                .password("")
+
+        Map props = [
+                'healthCheckProperties': [connectivityCheckTimeoutMs: '1000', 
expected99thPercentileMs: '10']
+        ]
+        builder.properties(props)
+
+        when:
+        HikariDataSource ds = (HikariDataSource) builder.build()
+
+        then:
+        ds.healthCheckProperties != null
+        ds.healthCheckProperties.getProperty('connectivityCheckTimeoutMs') == 
'1000'
+        ds.healthCheckProperties.getProperty('expected99thPercentileMs') == 
'10'
+
+        cleanup:
+        ds?.close()
+    }
+
+    def "HikariCP pool sizing properties are bound correctly"() {
+        given:
+        def builder = DataSourceBuilder.create()
+                .type(HikariDataSource)
+                .url("jdbc:h2:mem:poolSizeTest;DB_CLOSE_DELAY=-1")
+                .driverClassName("org.h2.Driver")
+                .username("sa")
+                .password("")
+        builder.properties([maximumPoolSize: '20', minimumIdle: '5'])
+
+        when:
+        HikariDataSource ds = (HikariDataSource) builder.build()
+
+        then:
+        ds.maximumPoolSize == 20
+        ds.minimumIdle == 5
+
+        cleanup:
+        ds?.close()
+    }
+
+    def "HikariCP timeout properties are bound correctly"() {
+        given:
+        def builder = DataSourceBuilder.create()
+                .type(HikariDataSource)
+                .url("jdbc:h2:mem:timeoutTest;DB_CLOSE_DELAY=-1")
+                .driverClassName("org.h2.Driver")
+                .username("sa")
+                .password("")
+        builder.properties([
+                connectionTimeout        : '15000',
+                idleTimeout              : '300000',
+                maxLifetime              : '900000',
+                validationTimeout        : '3000',
+                keepaliveTime            : '30000',
+                leakDetectionThreshold   : '60000',
+                initializationFailTimeout: '5000'
+        ])
+
+        when:
+        HikariDataSource ds = (HikariDataSource) builder.build()
+
+        then:
+        ds.connectionTimeout == 15000
+        ds.idleTimeout == 300000
+        ds.maxLifetime == 900000
+        ds.validationTimeout == 3000
+        ds.keepaliveTime == 30000
+        ds.leakDetectionThreshold == 60000
+        ds.initializationFailTimeout == 5000
+
+        cleanup:
+        ds?.close()
+    }
+
+    def "HikariCP connection behavior properties are bound correctly"() {
+        given:
+        def builder = DataSourceBuilder.create()
+                .type(HikariDataSource)
+                .url("jdbc:h2:mem:connBehaviorTest;DB_CLOSE_DELAY=-1")
+                .driverClassName("org.h2.Driver")
+                .username("sa")
+                .password("")
+        builder.properties([
+                autoCommit            : 'false',
+                connectionTestQuery   : 'SELECT 1',
+                connectionInitSql     : 'SET LOCK_TIMEOUT 5000',
+                transactionIsolation  : 'TRANSACTION_READ_COMMITTED',
+                catalog               : 'testCatalog',
+                schema                : 'testSchema',
+                isolateInternalQueries: 'true'
+        ])
+
+        when:
+        HikariDataSource ds = (HikariDataSource) builder.build()
+
+        then:
+        ds.autoCommit == false
+        ds.connectionTestQuery == 'SELECT 1'
+        ds.connectionInitSql == 'SET LOCK_TIMEOUT 5000'
+        ds.transactionIsolation == 'TRANSACTION_READ_COMMITTED'
+        ds.catalog == 'testCatalog'
+        ds.schema == 'testSchema'
+        ds.isolateInternalQueries == true
+
+        cleanup:
+        ds?.close()
+    }
+
+    def "HikariCP pool management properties are bound correctly"() {
+        given:
+        def builder = DataSourceBuilder.create()
+                .type(HikariDataSource)
+                .url("jdbc:h2:mem:poolMgmtTest;DB_CLOSE_DELAY=-1")
+                .driverClassName("org.h2.Driver")
+                .username("sa")
+                .password("")
+        builder.properties([
+                poolName      : 'TestPool',
+                registerMbeans: 'true'
+        ])
+
+        when:
+        HikariDataSource ds = (HikariDataSource) builder.build()
+
+        then:
+        ds.poolName == 'TestPool'
+        ds.registerMbeans == true
+
+        cleanup:
+        ds?.close()
+    }
+
+    def "HikariDataSource is fully configured from comprehensive Grails config 
map"() {
+        given: "a config map resembling a complete dataSource block from 
application.yml"
+        def builder = DataSourceBuilder.create()
+                .type(HikariDataSource)
+
+        Map config = [
+                url                      : 
"jdbc:h2:mem:fullHikariTest;DB_CLOSE_DELAY=-1",
+                driverClassName          : "org.h2.Driver",
+                username                 : "sa",
+                password                 : "",
+                maximumPoolSize          : '15',
+                minimumIdle              : '3',
+                connectionTimeout        : '20000',
+                idleTimeout              : '400000',
+                maxLifetime              : '1200000',
+                validationTimeout        : '4000',
+                keepaliveTime            : '60000',
+                leakDetectionThreshold   : '30000',
+                initializationFailTimeout: '2000',
+                autoCommit               : 'false',
+                connectionTestQuery      : 'SELECT 1',
+                connectionInitSql        : 'SET LOCK_TIMEOUT 5000',
+                poolName                 : 'GrailsPool',
+                registerMbeans           : 'true',
+                isolateInternalQueries   : 'true',
+                dbProperties             : [
+                        cachePrepStmts       : 'true',
+                        prepStmtCacheSize    : '250',
+                        prepStmtCacheSqlLimit: '2048',
+                        useSSL               : 'false'
+                ]
+        ]
+        builder.properties(config)
+
+        when:
+        HikariDataSource ds = (HikariDataSource) builder.build()
+
+        then: "all pool properties are configured"
+        ds.jdbcUrl == "jdbc:h2:mem:fullHikariTest;DB_CLOSE_DELAY=-1"
+        ds.username == "sa"
+        ds.maximumPoolSize == 15
+        ds.minimumIdle == 3
+
+        and: "all timeout properties are configured"
+        ds.connectionTimeout == 20000
+        ds.idleTimeout == 400000
+        ds.maxLifetime == 1200000
+        ds.validationTimeout == 4000
+        ds.keepaliveTime == 60000
+        ds.leakDetectionThreshold == 30000
+        ds.initializationFailTimeout == 2000
+
+        and: "all connection behavior properties are configured"
+        ds.autoCommit == false
+        ds.connectionTestQuery == 'SELECT 1'
+        ds.connectionInitSql == 'SET LOCK_TIMEOUT 5000'
+        ds.poolName == 'GrailsPool'
+        ds.registerMbeans == true
+        ds.isolateInternalQueries == true
+
+        and: "JDBC driver pass-through properties are bound"
+        ds.dataSourceProperties.getProperty('cachePrepStmts') == 'true'
+        ds.dataSourceProperties.getProperty('prepStmtCacheSize') == '250'
+        ds.dataSourceProperties.getProperty('prepStmtCacheSqlLimit') == '2048'
+        ds.dataSourceProperties.getProperty('useSSL') == 'false'
+
+        cleanup:
+        ds?.close()
+    }
 }

Reply via email to