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() + } }
