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

borinquenkid pushed a commit to branch chore/jdbc-package-cleanup
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 830f1992c8c1a33b5f1c139c7aec1f7287668815
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sat Aug 15 11:35:35 2026 -0500

    Add unit test coverage for the jdbc package in grails-datamapping-core
    
    Every previously-untested class under org.grails.datastore.gorm.jdbc
    (DatabaseDriver, MultiTenantConnection, MultiTenantDataSource,
    PropertyOrigin, OriginCapablePropertyValue, RelaxedNames,
    RelaxedConversionService, RelaxedDataBinder) and its connections
    subpackage (DataSourceSettings, DataSourceSettingsBuilder,
    DataSourceConnectionSource, DataSourceConnectionSourceFactory,
    CachedDataSourceConnectionSourceFactory,
    SpringDataSourceConnectionSourceFactory) now has a Spock spec.
    
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 .../datastore/gorm/jdbc/DatabaseDriverSpec.groovy  |  89 +++++++++++++++
 .../gorm/jdbc/MultiTenantConnectionSpec.groovy     | 104 +++++++++++++++++
 .../gorm/jdbc/MultiTenantDataSourceSpec.groovy     |  70 ++++++++++++
 .../jdbc/OriginCapablePropertyValueSpec.groovy     |  90 +++++++++++++++
 .../datastore/gorm/jdbc/PropertyOriginSpec.groovy  |  49 ++++++++
 .../gorm/jdbc/RelaxedConversionServiceSpec.groovy  | 116 +++++++++++++++++++
 .../gorm/jdbc/RelaxedDataBinderSpec.groovy         | 123 +++++++++++++++++++++
 .../datastore/gorm/jdbc/RelaxedNamesSpec.groovy    |  77 +++++++++++++
 ...hedDataSourceConnectionSourceFactorySpec.groovy |  58 ++++++++++
 .../DataSourceConnectionSourceFactorySpec.groovy   | 115 +++++++++++++++++++
 .../DataSourceConnectionSourceSpec.groovy          |  96 ++++++++++++++++
 .../DataSourceSettingsBuilderSpec.groovy           |  75 +++++++++++++
 .../jdbc/connections/DataSourceSettingsSpec.groovy | 123 +++++++++++++++++++++
 ...ingDataSourceConnectionSourceFactorySpec.groovy |  82 ++++++++++++++
 14 files changed, 1267 insertions(+)

diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/DatabaseDriverSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/DatabaseDriverSpec.groovy
new file mode 100644
index 0000000000..a120f32380
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/DatabaseDriverSpec.groovy
@@ -0,0 +1,89 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class DatabaseDriverSpec extends Specification {
+
+    @Unroll
+    void "fromJdbcUrl resolves '#url' to #expected"() {
+        expect:
+        DatabaseDriver.fromJdbcUrl(url) == expected
+
+        where:
+        url                                             | expected
+        'jdbc:h2:mem:testDb'                             | DatabaseDriver.H2
+        'jdbc:mysql://localhost:3306/test'               | DatabaseDriver.MYSQL
+        'jdbc:mariadb://localhost:3306/test'              | 
DatabaseDriver.MARIADB
+        'jdbc:postgresql://localhost:5432/test'           | 
DatabaseDriver.POSTGRESQL
+        'jdbc:oracle:thin:@localhost:1521:orcl'           | 
DatabaseDriver.ORACLE
+        'jdbc:sqlserver://localhost:1433;databaseName=x'  | 
DatabaseDriver.SQLSERVER
+        'jdbc:hsqldb:mem:testDb'                          | 
DatabaseDriver.HSQLDB
+        'jdbc:sqlite:test.db'                             | 
DatabaseDriver.SQLITE
+        'jdbc:derby:memory:testDb'                        | 
DatabaseDriver.DERBY
+        'jdbc:unknowndb:foo'                              | 
DatabaseDriver.UNKNOWN
+        null                                              | 
DatabaseDriver.UNKNOWN
+        ''                                                | 
DatabaseDriver.UNKNOWN
+    }
+
+    void "fromJdbcUrl throws IllegalArgumentException for a URL not starting 
with jdbc"() {
+        when:
+        DatabaseDriver.fromJdbcUrl('mysql://localhost:3306/test')
+
+        then:
+        thrown(IllegalArgumentException)
+    }
+
+    @Unroll
+    void "fromProductName resolves '#productName' to #expected"() {
+        expect:
+        DatabaseDriver.fromProductName(productName) == expected
+
+        where:
+        productName                    | expected
+        'H2'                           | DatabaseDriver.H2
+        'h2'                           | DatabaseDriver.H2
+        'MySQL'                        | DatabaseDriver.MYSQL
+        'PostgreSQL'                   | DatabaseDriver.POSTGRESQL
+        'Oracle'                       | DatabaseDriver.ORACLE
+        'Firebird 3.0'                 | DatabaseDriver.FIREBIRD
+        'DB2/LINUXX8664'               | DatabaseDriver.DB2
+        'DB2 UDB for AS/400'           | DatabaseDriver.DB2_AS400
+        'Something running on AS/400'  | DatabaseDriver.DB2_AS400
+        'Completely unknown product'   | DatabaseDriver.UNKNOWN
+        null                           | DatabaseDriver.UNKNOWN
+        ''                             | DatabaseDriver.UNKNOWN
+    }
+
+    void "getDriverClassName and getValidationQuery and 
getXaDataSourceClassName return the expected values for H2"() {
+        expect:
+        DatabaseDriver.H2.driverClassName == 'org.h2.Driver'
+        DatabaseDriver.H2.xaDataSourceClassName == 
'org.h2.jdbcx.JdbcDataSource'
+        DatabaseDriver.H2.validationQuery == 'SELECT 1'
+    }
+
+    void "UNKNOWN driver has no driver class name, xa data source or 
validation query"() {
+        expect:
+        DatabaseDriver.UNKNOWN.driverClassName == null
+        DatabaseDriver.UNKNOWN.xaDataSourceClassName == null
+        DatabaseDriver.UNKNOWN.validationQuery == null
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnectionSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnectionSpec.groovy
new file mode 100644
index 0000000000..491f2daf1f
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnectionSpec.groovy
@@ -0,0 +1,104 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc
+
+import java.sql.Connection
+
+import org.grails.datastore.gorm.jdbc.schema.SchemaHandler
+import spock.lang.Specification
+
+class MultiTenantConnectionSpec extends Specification {
+
+    void "close restores the default schema before delegating to the target 
connection when open"() {
+        given:
+        def target = Mock(Connection) { isClosed() >> false }
+        def schemaHandler = Mock(SchemaHandler)
+        def connection = new MultiTenantConnection(target, schemaHandler)
+
+        when:
+        connection.close()
+
+        then:
+        1 * schemaHandler.useDefaultSchema(connection)
+        1 * target.close()
+    }
+
+    void "close does not restore the default schema when the connection is 
already closed"() {
+        given:
+        def target = Mock(Connection) { isClosed() >> true }
+        def schemaHandler = Mock(SchemaHandler)
+        def connection = new MultiTenantConnection(target, schemaHandler)
+
+        when:
+        connection.close()
+
+        then:
+        0 * schemaHandler.useDefaultSchema(_)
+        1 * target.close()
+    }
+
+    void "close still closes the target connection when restoring the schema 
throws"() {
+        given:
+        def target = Mock(Connection) { isClosed() >> false }
+        def schemaHandler = Mock(SchemaHandler) {
+            useDefaultSchema(_) >> { throw new RuntimeException('boom') }
+        }
+        def connection = new MultiTenantConnection(target, schemaHandler)
+
+        when:
+        connection.close()
+
+        then:
+        thrown(RuntimeException)
+        1 * target.close()
+    }
+
+    void "other Connection methods delegate to the target connection"() {
+        given:
+        def target = Mock(Connection)
+        def schemaHandler = Mock(SchemaHandler)
+        def connection = new MultiTenantConnection(target, schemaHandler)
+
+        when:
+        connection.isReadOnly()
+
+        then:
+        1 * target.isReadOnly() >> true
+
+        when:
+        def autoCommit = connection.getAutoCommit()
+
+        then:
+        1 * target.getAutoCommit() >> false
+        !autoCommit
+    }
+
+    void "target and schemaHandler are exposed"() {
+        given:
+        def target = Mock(Connection)
+        def schemaHandler = Mock(SchemaHandler)
+
+        when:
+        def connection = new MultiTenantConnection(target, schemaHandler)
+
+        then:
+        connection.target.is(target)
+        connection.schemaHandler.is(schemaHandler)
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantDataSourceSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantDataSourceSpec.groovy
new file mode 100644
index 0000000000..0a55213cf5
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantDataSourceSpec.groovy
@@ -0,0 +1,70 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc
+
+import javax.sql.DataSource
+
+import spock.lang.Specification
+
+class MultiTenantDataSourceSpec extends Specification {
+
+    void "target and tenantId are exposed"() {
+        given:
+        def target = Mock(DataSource)
+
+        when:
+        def dataSource = new MultiTenantDataSource(target, 'tenantA')
+
+        then:
+        dataSource.target.is(target)
+        dataSource.tenantId == 'tenantA'
+    }
+
+    void "getConnection delegates to the target data source"() {
+        given:
+        def target = Mock(DataSource)
+        def connection = Mock(java.sql.Connection)
+        def dataSource = new MultiTenantDataSource(target, 'tenantA')
+
+        when:
+        def result = dataSource.getConnection()
+
+        then:
+        1 * target.getConnection() >> connection
+        result.is(connection)
+    }
+
+    void "two instances with the same tenantId are equal regardless of 
target"() {
+        given:
+        def targetA = Mock(DataSource)
+        def targetB = Mock(DataSource)
+
+        expect:
+        new MultiTenantDataSource(targetA, 'tenantA') == new 
MultiTenantDataSource(targetB, 'tenantA')
+        new MultiTenantDataSource(targetA, 'tenantA').hashCode() == new 
MultiTenantDataSource(targetB, 'tenantA').hashCode()
+    }
+
+    void "two instances with different tenantIds are not equal"() {
+        given:
+        def target = Mock(DataSource)
+
+        expect:
+        new MultiTenantDataSource(target, 'tenantA') != new 
MultiTenantDataSource(target, 'tenantB')
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/OriginCapablePropertyValueSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/OriginCapablePropertyValueSpec.groovy
new file mode 100644
index 0000000000..6e71bcbbf3
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/OriginCapablePropertyValueSpec.groovy
@@ -0,0 +1,90 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc
+
+import org.springframework.beans.PropertyValue
+import org.springframework.core.env.PropertySource
+import spock.lang.Specification
+
+class OriginCapablePropertyValueSpec extends Specification {
+
+    private static PropertySource<String> namedSource(String name) {
+        new PropertySource<String>(name, 'value') {
+            @Override
+            Object getProperty(String propertyName) { null }
+        }
+    }
+
+    void "getName and getValue expose the values passed to the constructor"() {
+        given:
+        def origin = new PropertyOrigin(namedSource('mySource'), 
'original.name')
+
+        when:
+        def propertyValue = new OriginCapablePropertyValue('bound.name', 
'boundValue', origin)
+
+        then:
+        propertyValue.name == 'bound.name'
+        propertyValue.value == 'boundValue'
+    }
+
+    void "toString includes the origin name and the source name"() {
+        given:
+        def origin = new PropertyOrigin(namedSource('myPropertySource'), 
'original.name')
+        def propertyValue = new OriginCapablePropertyValue('bound.name', 
'boundValue', origin)
+
+        expect:
+        propertyValue.toString() == "'original.name' from 'myPropertySource'"
+    }
+
+    void "toString reports an unknown source when the origin has no source"() {
+        given:
+        def origin = new PropertyOrigin(null, 'original.name')
+        def propertyValue = new OriginCapablePropertyValue('bound.name', 
'boundValue', origin)
+
+        expect:
+        propertyValue.toString() == "'original.name' from 'unknown'"
+    }
+
+    void "static getOrigin returns the origin of an OriginCapablePropertyValue 
directly"() {
+        given:
+        def origin = new PropertyOrigin(namedSource('myPropertySource'), 
'original.name')
+        def propertyValue = new OriginCapablePropertyValue('bound.name', 
'boundValue', origin)
+
+        expect:
+        OriginCapablePropertyValue.getOrigin(propertyValue).is(origin)
+    }
+
+    void "static getOrigin returns the propertyOrigin attribute for a plain 
PropertyValue"() {
+        given:
+        def origin = new PropertyOrigin(namedSource('myPropertySource'), 
'original.name')
+        def propertyValue = new PropertyValue('bound.name', 'boundValue')
+        propertyValue.setAttribute('propertyOrigin', origin)
+
+        expect:
+        OriginCapablePropertyValue.getOrigin(propertyValue).is(origin)
+    }
+
+    void "static getOrigin returns null for a plain PropertyValue with no 
propertyOrigin attribute"() {
+        given:
+        def propertyValue = new PropertyValue('bound.name', 'boundValue')
+
+        expect:
+        OriginCapablePropertyValue.getOrigin(propertyValue) == null
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/PropertyOriginSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/PropertyOriginSpec.groovy
new file mode 100644
index 0000000000..c4a25a5acc
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/PropertyOriginSpec.groovy
@@ -0,0 +1,49 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc
+
+import org.springframework.core.env.PropertySource
+import spock.lang.Specification
+
+class PropertyOriginSpec extends Specification {
+
+    void "getSource and getName return the values passed to the constructor"() 
{
+        given:
+        def source = new PropertySource<String>('mySource', 'value') {
+            @Override
+            Object getProperty(String name) { null }
+        }
+
+        when:
+        def origin = new PropertyOrigin(source, 'my.original.name')
+
+        then:
+        origin.source.is(source)
+        origin.name == 'my.original.name'
+    }
+
+    void "source may be null"() {
+        when:
+        def origin = new PropertyOrigin(null, 'my.original.name')
+
+        then:
+        origin.source == null
+        origin.name == 'my.original.name'
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedConversionServiceSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedConversionServiceSpec.groovy
new file mode 100644
index 0000000000..248d70abee
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedConversionServiceSpec.groovy
@@ -0,0 +1,116 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc
+
+import org.springframework.core.convert.ConversionFailedException
+import org.springframework.core.convert.TypeDescriptor
+import org.springframework.core.convert.support.DefaultConversionService
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class RelaxedConversionServiceSpec extends Specification {
+
+    static enum Color {
+        RED, GREEN_BLUE
+    }
+
+    void "convert delegates to the provided root conversionService when it can 
handle the conversion"() {
+        given:
+        def service = new RelaxedConversionService(new 
DefaultConversionService())
+
+        expect:
+        service.convert('42', Integer) == 42
+        service.canConvert(String, Integer)
+    }
+
+    void "canConvert(TypeDescriptor, TypeDescriptor) delegates to the provided 
root conversionService"() {
+        given:
+        def service = new RelaxedConversionService(new 
DefaultConversionService())
+
+        expect:
+        service.canConvert(TypeDescriptor.valueOf(String), 
TypeDescriptor.valueOf(Integer))
+    }
+
+    void "convert falls back to the additional converters for String to char[] 
which the root does not support"() {
+        given:
+        def service = new RelaxedConversionService(new 
DefaultConversionService())
+
+        expect:
+        service.convert('abcde', char[].class) == 'abcde'.toCharArray()
+    }
+
+    void "convert works with a null root conversionService using only the 
additional converters"() {
+        given:
+        def service = new RelaxedConversionService(null)
+
+        expect:
+        service.convert('abcde', char[].class) == 'abcde'.toCharArray()
+        service.canConvert(String, char[].class)
+    }
+
+    static class Unconvertible {
+        private Unconvertible() {}
+    }
+
+    void "canConvert returns false for an unsupported conversion"() {
+        given:
+        def service = new RelaxedConversionService(new 
DefaultConversionService())
+
+        expect:
+        !service.canConvert(String, Unconvertible)
+    }
+
+    @Unroll
+    void "convert resolves '#source' to enum #expected ignoring case and 
separator style"() {
+        given:
+        def service = new RelaxedConversionService(new 
DefaultConversionService())
+
+        expect:
+        service.convert(source, Color) == expected
+
+        where:
+        source        | expected
+        'RED'         | Color.RED
+        'red'         | Color.RED
+        'Red'         | Color.RED
+        'GREEN_BLUE'  | Color.GREEN_BLUE
+        'green-blue'  | Color.GREEN_BLUE
+        'green_blue'  | Color.GREEN_BLUE
+    }
+
+    void "convert returns null for an empty string when the target is an 
enum"() {
+        given:
+        def service = new RelaxedConversionService(new 
DefaultConversionService())
+
+        expect:
+        service.convert('', Color) == null
+    }
+
+    void "convert throws a ConversionFailedException for an unrecognized enum 
constant"() {
+        given:
+        def service = new RelaxedConversionService(new 
DefaultConversionService())
+
+        when:
+        service.convert('not-a-color', Color)
+
+        then:
+        def ex = thrown(ConversionFailedException)
+        ex.cause instanceof IllegalArgumentException
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedDataBinderSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedDataBinderSpec.groovy
new file mode 100644
index 0000000000..df6f25c8fc
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedDataBinderSpec.groovy
@@ -0,0 +1,123 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc
+
+import org.springframework.beans.MutablePropertyValues
+import spock.lang.Specification
+
+class RelaxedDataBinderSpec extends Specification {
+
+    static class NestedBean {
+        String value
+    }
+
+    static class SimpleBean {
+        String fooBar
+        String jdbcUrl
+        Map<String, Object> options = [:]
+        NestedBean nested
+    }
+
+    void "a dash separated property name is bound to the matching camelCase 
property"() {
+        given:
+        def target = new SimpleBean()
+        def binder = new RelaxedDataBinder(target)
+        def values = new MutablePropertyValues()
+        values.add('foo-bar', 'baz')
+
+        when:
+        binder.bind(values)
+
+        then:
+        target.fooBar == 'baz'
+    }
+
+    void "an underscore separated property name is bound to the matching 
camelCase property"() {
+        given:
+        def target = new SimpleBean()
+        def binder = new RelaxedDataBinder(target)
+        def values = new MutablePropertyValues()
+        values.add('foo_bar', 'baz')
+
+        when:
+        binder.bind(values)
+
+        then:
+        target.fooBar == 'baz'
+    }
+
+    void "withAlias resolves an alternate incoming property name to the target 
property"() {
+        given:
+        def target = new SimpleBean()
+        def binder = new RelaxedDataBinder(target).withAlias('url', 'jdbcUrl')
+        def values = new MutablePropertyValues()
+        values.add('url', 'jdbc:h2:mem:test')
+
+        when:
+        binder.bind(values)
+
+        then:
+        target.jdbcUrl == 'jdbc:h2:mem:test'
+    }
+
+    void "binding to a Map target populates the map with the given keys"() {
+        given:
+        def map = [:]
+        def binder = new RelaxedDataBinder(map)
+        def values = new MutablePropertyValues()
+        values.add('key', 'value')
+
+        when:
+        binder.bind(values)
+
+        then:
+        map.key == 'value'
+    }
+
+    void "a null nested bean property is auto-instantiated and populated"() {
+        given:
+        def target = new SimpleBean()
+        def binder = new RelaxedDataBinder(target)
+        def values = new MutablePropertyValues()
+        values.add('nested.value', 'hello')
+
+        when:
+        binder.bind(values)
+
+        then:
+        target.nested != null
+        target.nested.value == 'hello'
+    }
+
+    void "period separated keys are bound into an existing map property"() {
+        given:
+        def target = new SimpleBean()
+        def binder = new RelaxedDataBinder(target)
+        def values = new MutablePropertyValues()
+        values.add('options.timeout', '30')
+        values.add('options.retries', '3')
+
+        when:
+        binder.bind(values)
+
+        then:
+        target.options.timeout == '30'
+        target.options.retries == '3'
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedNamesSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedNamesSpec.groovy
new file mode 100644
index 0000000000..19b4ccc156
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedNamesSpec.groovy
@@ -0,0 +1,77 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class RelaxedNamesSpec extends Specification {
+
+    private static Set<String> namesFor(String source) {
+        new RelaxedNames(source).iterator().toList() as Set
+    }
+
+    void "dash separated names produce underscore, camelCase and case 
variations"() {
+        expect:
+        namesFor('foo-bar') == ['foo-bar', 'foo_bar', 'fooBar', 'foobar', 
'FOO-BAR', 'FOO_BAR', 'FOOBAR'] as Set
+    }
+
+    void "camelCase names produce dash, underscore and lowercase/uppercase 
variations"() {
+        expect:
+        namesFor('fooBar') == ['fooBar', 'foo_bar', 'foo-bar', 'foobar', 
'FOOBAR', 'FOO_BAR', 'FOO-BAR'] as Set
+    }
+
+    void "a single lowercase word only produces case variations"() {
+        expect:
+        namesFor('foo') == ['foo', 'FOO'] as Set
+    }
+
+    void "null name is treated as an empty string"() {
+        expect:
+        namesFor(null) == [''] as Set
+    }
+
+    void "empty name only produces itself"() {
+        expect:
+        namesFor('') == [''] as Set
+    }
+
+    @Unroll
+    void "'#source' variations include '#expected'"() {
+        expect:
+        namesFor(source).contains(expected)
+
+        where:
+        source        | expected
+        'db-create'   | 'dbCreate'
+        'db-create'   | 'db_create'
+        'db-create'   | 'DB-CREATE'
+        'dbCreate'    | 'db-create'
+        'dbCreate'    | 'db_create'
+        'DB_CREATE'   | 'dbCreate'
+    }
+
+    void "iterating twice returns the same values"() {
+        given:
+        def names = new RelaxedNames('foo-bar')
+
+        expect:
+        names.iterator().toList() == names.iterator().toList()
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/CachedDataSourceConnectionSourceFactorySpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/CachedDataSourceConnectionSourceFactorySpec.groovy
new file mode 100644
index 0000000000..363ecf58f8
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/CachedDataSourceConnectionSourceFactorySpec.groovy
@@ -0,0 +1,58 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc.connections
+
+import spock.lang.Specification
+
+class CachedDataSourceConnectionSourceFactorySpec extends Specification {
+
+    void "create(name, settings) returns the same cached instance for the same 
name"() {
+        given:
+        def factory = new CachedDataSourceConnectionSourceFactory()
+        def settings = new DataSourceSettings(url: 
'jdbc:h2:mem:cachedFactoryTest1;DB_CLOSE_DELAY=-1')
+
+        when:
+        def first = factory.create('default', settings)
+        def second = factory.create('default', settings)
+
+        then:
+        first.is(second)
+
+        cleanup:
+        first?.close()
+    }
+
+    void "create(name, settings) returns a different instance for a different 
name"() {
+        given:
+        def factory = new CachedDataSourceConnectionSourceFactory()
+        def settingsA = new DataSourceSettings(url: 
'jdbc:h2:mem:cachedFactoryTest2a;DB_CLOSE_DELAY=-1')
+        def settingsB = new DataSourceSettings(url: 
'jdbc:h2:mem:cachedFactoryTest2b;DB_CLOSE_DELAY=-1')
+
+        when:
+        def a = factory.create('sourceA', settingsA)
+        def b = factory.create('sourceB', settingsB)
+
+        then:
+        !a.is(b)
+
+        cleanup:
+        a?.close()
+        b?.close()
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceFactorySpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceFactorySpec.groovy
new file mode 100644
index 0000000000..0a9730c393
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceFactorySpec.groovy
@@ -0,0 +1,115 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc.connections
+
+import org.grails.datastore.mapping.core.DatastoreUtils
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy
+import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
+import spock.lang.Specification
+
+class DataSourceConnectionSourceFactorySpec extends Specification {
+
+    void "create resolves settings for the default data source from the 
'dataSource' prefix"() {
+        given:
+        def factory = new DataSourceConnectionSourceFactory()
+        def config = DatastoreUtils.createPropertyResolver([
+                'dataSource.url'    : 
'jdbc:h2:mem:factoryDefaultTest;DB_CLOSE_DELAY=-1',
+                'dataSource.lazy'   : 'false',
+                'dataSource.transactionAware': 'false'
+        ])
+
+        when:
+        def connectionSource = factory.create(ConnectionSource.DEFAULT, config)
+
+        then:
+        connectionSource.name == ConnectionSource.DEFAULT
+        connectionSource.settings.url == 
'jdbc:h2:mem:factoryDefaultTest;DB_CLOSE_DELAY=-1'
+        connectionSource.source != null
+
+        cleanup:
+        connectionSource?.close()
+    }
+
+    void "create resolves settings for a named data source from the 
'dataSources.<name>' prefix"() {
+        given:
+        def factory = new DataSourceConnectionSourceFactory()
+        def config = DatastoreUtils.createPropertyResolver([
+                'dataSources.secondary.url'          : 
'jdbc:h2:mem:factoryNamedTest;DB_CLOSE_DELAY=-1',
+                'dataSources.secondary.lazy'          : 'false',
+                'dataSources.secondary.transactionAware': 'false'
+        ])
+
+        when:
+        def connectionSource = factory.create('secondary', config)
+
+        then:
+        connectionSource.name == 'secondary'
+        connectionSource.settings.url == 
'jdbc:h2:mem:factoryNamedTest;DB_CLOSE_DELAY=-1'
+
+        cleanup:
+        connectionSource?.close()
+    }
+
+    void "create(name, settings) wraps the built DataSource with lazy and 
transaction-aware proxies when configured"() {
+        given:
+        def factory = new DataSourceConnectionSourceFactory()
+        def settings = new DataSourceSettings(
+                url: 'jdbc:h2:mem:factoryProxyTest;DB_CLOSE_DELAY=-1',
+                lazy: true,
+                transactionAware: true)
+
+        when:
+        def connectionSource = factory.create('default', settings)
+
+        then:
+        connectionSource.source instanceof TransactionAwareDataSourceProxy
+        ((TransactionAwareDataSourceProxy) 
connectionSource.source).targetDataSource instanceof 
LazyConnectionDataSourceProxy
+
+        cleanup:
+        connectionSource?.close()
+    }
+
+    void "create(name, settings) does not wrap the DataSource when lazy and 
transactionAware are disabled"() {
+        given:
+        def factory = new DataSourceConnectionSourceFactory()
+        def settings = new DataSourceSettings(
+                url: 'jdbc:h2:mem:factoryNoProxyTest;DB_CLOSE_DELAY=-1',
+                lazy: false,
+                transactionAware: false)
+
+        when:
+        def connectionSource = factory.create('default', settings)
+
+        then:
+        !(connectionSource.source instanceof TransactionAwareDataSourceProxy)
+        !(connectionSource.source instanceof LazyConnectionDataSourceProxy)
+
+        cleanup:
+        connectionSource?.close()
+    }
+
+    void "getConnectionSourcesConfigurationKey returns the dataSources 
settings key"() {
+        given:
+        def factory = new DataSourceConnectionSourceFactory()
+
+        expect:
+        factory.connectionSourcesConfigurationKey == 'dataSources'
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceSpec.groovy
new file mode 100644
index 0000000000..9b1c127ef3
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceSpec.groovy
@@ -0,0 +1,96 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc.connections
+
+import javax.sql.DataSource
+
+import org.springframework.jdbc.datasource.DelegatingDataSource
+import spock.lang.Specification
+
+class DataSourceConnectionSourceSpec extends Specification {
+
+    static interface CloseableDataSource extends DataSource {
+        void close()
+    }
+
+    void "close invokes a public close() method on the underlying DataSource 
when present"() {
+        given:
+        def dataSource = Mock(CloseableDataSource)
+        def source = new DataSourceConnectionSource('test', dataSource, new 
DataSourceSettings())
+
+        when:
+        source.close()
+
+        then:
+        1 * dataSource.close()
+    }
+
+    void "close unwraps a DelegatingDataSource to find a close() method on the 
target"() {
+        given:
+        def closeable = Mock(CloseableDataSource)
+        def delegating = new DelegatingDataSource(closeable)
+        def source = new DataSourceConnectionSource('test', delegating, new 
DataSourceSettings())
+
+        when:
+        source.close()
+
+        then:
+        1 * closeable.close()
+    }
+
+    void "close does not fail when the DataSource has no close() method"() {
+        given:
+        def dataSource = Mock(DataSource)
+        def source = new DataSourceConnectionSource('test', dataSource, new 
DataSourceSettings())
+
+        when:
+        source.close()
+
+        then:
+        noExceptionThrown()
+    }
+
+    void "close swallows exceptions thrown by the underlying close() method"() 
{
+        given:
+        def dataSource = Mock(CloseableDataSource) {
+            close() >> { throw new RuntimeException('boom') }
+        }
+        def source = new DataSourceConnectionSource('test', dataSource, new 
DataSourceSettings())
+
+        when:
+        source.close()
+
+        then:
+        noExceptionThrown()
+    }
+
+    void "getName, getSource and getSettings expose the constructor 
arguments"() {
+        given:
+        def dataSource = Mock(DataSource)
+        def settings = new DataSourceSettings()
+
+        when:
+        def source = new DataSourceConnectionSource('test', dataSource, 
settings)
+
+        then:
+        source.name == 'test'
+        source.source.is(dataSource)
+        source.settings.is(settings)
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsBuilderSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsBuilderSpec.groovy
new file mode 100644
index 0000000000..24a35ba707
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsBuilderSpec.groovy
@@ -0,0 +1,75 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc.connections
+
+import org.grails.datastore.mapping.core.DatastoreUtils
+import spock.lang.Specification
+
+class DataSourceSettingsBuilderSpec extends Specification {
+
+    void "build resolves settings from the default 'dataSource' configuration 
prefix"() {
+        given:
+        def config = DatastoreUtils.createPropertyResolver([
+                'dataSource.url'     : 'jdbc:h2:mem:builderDefaultTest',
+                'dataSource.username': 'sa',
+                'dataSource.password': 'secret',
+                'dataSource.pooled'  : 'false'
+        ])
+
+        when:
+        def settings = new DataSourceSettingsBuilder(config).build()
+
+        then:
+        settings.url == 'jdbc:h2:mem:builderDefaultTest'
+        settings.username == 'sa'
+        settings.password == 'secret'
+        !settings.pooled
+    }
+
+    void "build resolves settings from a custom configuration prefix"() {
+        given:
+        def config = DatastoreUtils.createPropertyResolver([
+                'dataSources.secondary.url'     : 
'jdbc:h2:mem:builderCustomPrefixTest',
+                'dataSources.secondary.username' : 'other'
+        ])
+
+        when:
+        def settings = new DataSourceSettingsBuilder(config, 
'dataSources.secondary').build()
+
+        then:
+        settings.url == 'jdbc:h2:mem:builderCustomPrefixTest'
+        settings.username == 'other'
+    }
+
+    void "properties not present in configuration keep their default values"() 
{
+        given:
+        def config = DatastoreUtils.createPropertyResolver([
+                'dataSource.url': 'jdbc:h2:mem:builderDefaultsTest'
+        ])
+
+        when:
+        def settings = new DataSourceSettingsBuilder(config).build()
+
+        then:
+        settings.pooled
+        settings.lazy
+        settings.transactionAware
+        settings.dbCreate == 'none'
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsSpec.groovy
new file mode 100644
index 0000000000..c6a7e04032
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsSpec.groovy
@@ -0,0 +1,123 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc.connections
+
+import org.grails.datastore.gorm.jdbc.schema.DefaultSchemaHandler
+import spock.lang.Specification
+
+class DataSourceSettingsSpec extends Specification {
+
+    void "default settings use an in-memory H2 database and the default schema 
handler"() {
+        given:
+        def settings = new DataSourceSettings()
+
+        expect:
+        settings.url == 'jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000'
+        settings.pooled
+        settings.lazy
+        settings.transactionAware
+        !settings.readOnly
+        !settings.logSql
+        !settings.formatSql
+        settings.dbCreate == 'none'
+        settings.schemaHandler == DefaultSchemaHandler
+        settings.properties == [:]
+    }
+
+    void "toHibernateProperties maps the dbCreate, logSql and formatSql 
settings"() {
+        given:
+        def settings = new DataSourceSettings(dbCreate: 'update', logSql: 
true, formatSql: true)
+
+        when:
+        def props = settings.toHibernateProperties()
+
+        then:
+        props.getProperty('hibernate.hbm2ddl.auto') == 'update'
+        props.getProperty('hibernate.show_sql') == 'true'
+        props.getProperty('hibernate.format_sql') == 'true'
+        !props.containsKey('hibernate.dialect')
+    }
+
+    void "toHibernateProperties includes the dialect name when a dialect class 
is configured"() {
+        given:
+        def settings = new DataSourceSettings(dialect: String)
+
+        when:
+        def props = settings.toHibernateProperties()
+
+        then:
+        props.getProperty('hibernate.dialect') == String.name
+    }
+
+    void "toProperties includes url, driverClassName, username and password 
when set"() {
+        given:
+        def settings = new DataSourceSettings(
+                url: 'jdbc:h2:mem:test',
+                driverClassName: 'org.h2.Driver',
+                username: 'sa',
+                password: 'secret')
+
+        when:
+        def props = settings.toProperties()
+
+        then:
+        props.url == 'jdbc:h2:mem:test'
+        props.driverClassName == 'org.h2.Driver'
+        props.username == 'sa'
+        props.password == 'secret'
+        !props.containsKey('defaultReadOnly')
+    }
+
+    void "toProperties omits driverClassName, username and password when not 
set"() {
+        given:
+        def settings = new DataSourceSettings(url: 'jdbc:h2:mem:test')
+
+        when:
+        def props = settings.toProperties()
+
+        then:
+        props.url == 'jdbc:h2:mem:test'
+        !props.containsKey('driverClassName')
+        !props.containsKey('username')
+        !props.containsKey('password')
+    }
+
+    void "toProperties includes defaultReadOnly when readOnly is true"() {
+        given:
+        def settings = new DataSourceSettings(url: 'jdbc:h2:mem:test', 
readOnly: true)
+
+        when:
+        def props = settings.toProperties()
+
+        then:
+        props.defaultReadOnly == 'true'
+    }
+
+    void "toProperties merges in the configured additional properties"() {
+        given:
+        def settings = new DataSourceSettings(url: 'jdbc:h2:mem:test', 
properties: [maximumPoolSize: '10'])
+
+        when:
+        def props = settings.toProperties()
+
+        then:
+        props.maximumPoolSize == '10'
+        props.url == 'jdbc:h2:mem:test'
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/SpringDataSourceConnectionSourceFactorySpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/SpringDataSourceConnectionSourceFactorySpec.groovy
new file mode 100644
index 0000000000..93c44e953f
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/SpringDataSourceConnectionSourceFactorySpec.groovy
@@ -0,0 +1,82 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.jdbc.connections
+
+import javax.sql.DataSource
+
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.springframework.context.support.StaticApplicationContext
+import spock.lang.Specification
+
+class SpringDataSourceConnectionSourceFactorySpec extends Specification {
+
+    void "create returns the Spring-managed 'dataSource' bean for the default 
connection source name"() {
+        given:
+        def springDataSource = Mock(DataSource)
+        def context = new StaticApplicationContext()
+        context.beanFactory.registerSingleton('dataSource', springDataSource)
+        context.refresh()
+
+        def factory = new SpringDataSourceConnectionSourceFactory()
+        factory.applicationContext = context
+
+        when:
+        def connectionSource = factory.create(ConnectionSource.DEFAULT, new 
DataSourceSettings())
+
+        then:
+        connectionSource.source.is(springDataSource)
+    }
+
+    void "create returns the Spring-managed 'dataSource_<name>' bean for a 
named connection source"() {
+        given:
+        def springDataSource = Mock(DataSource)
+        def context = new StaticApplicationContext()
+        context.beanFactory.registerSingleton('dataSource_secondary', 
springDataSource)
+        context.refresh()
+
+        def factory = new SpringDataSourceConnectionSourceFactory()
+        factory.applicationContext = context
+
+        when:
+        def connectionSource = factory.create('secondary', new 
DataSourceSettings())
+
+        then:
+        connectionSource.source.is(springDataSource)
+    }
+
+    void "create falls back to building its own DataSource when no matching 
Spring bean exists"() {
+        given:
+        def context = new StaticApplicationContext()
+        context.refresh()
+
+        def factory = new SpringDataSourceConnectionSourceFactory()
+        factory.applicationContext = context
+        def settings = new DataSourceSettings(url: 
'jdbc:h2:mem:springFallbackTest;DB_CLOSE_DELAY=-1')
+
+        when:
+        def connectionSource = factory.create(ConnectionSource.DEFAULT, 
settings)
+
+        then:
+        connectionSource.source instanceof DataSource
+        connectionSource instanceof DataSourceConnectionSource
+
+        cleanup:
+        connectionSource?.close()
+    }
+}

Reply via email to