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

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 4a3aadd12563186a1262c299a4cde197127d68ea
Author: Andrus Adamchik <[email protected]>
AuthorDate: Tue Jul 21 15:41:31 2026 +0200

    CAY-2983 CayenneDataSource: Public-facing DataSource builder
---
 README.md                                          |    7 +-
 RELEASE-NOTES.txt                                  |    1 +
 .../org/apache/cayenne/tools/DbGenerateTask.java   |    6 +-
 .../configuration/DefaultRuntimeProperties.java    |   13 +
 .../cayenne/configuration/RuntimeProperties.java   |   11 +-
 .../runtime/PropertyDataSourceFactory.java         |   69 +-
 .../runtime/XMLPoolingDataSourceFactory.java       |   13 +-
 .../cayenne/datasource/CayenneDataSource.java      |  301 ++++++
 .../cayenne/datasource/DataSourceBuilder.java      |    4 +-
 .../cayenne/datasource/DriverDataSource.java       |  262 +++--
 .../datasource/ManagedPoolingDataSource.java       |   11 +-
 .../cayenne/datasource/PoolAwareConnection.java    | 1071 ++++++++++----------
 .../cayenne/datasource/PoolingDataSource.java      |    3 +-
 .../datasource/PoolingDataSourceBuilder.java       |    8 +-
 .../cayenne/runtime/CayenneRuntimeBuilder.java     |    4 +-
 .../cayenne/datasource/CayenneDataSourceTest.java  |  196 ++++
 .../org/apache/cayenne/unit/CayenneTestsEnv.java   |    8 +-
 .../_getting-started-db-first/part4-java-code.adoc |    5 +-
 18 files changed, 1223 insertions(+), 770 deletions(-)

diff --git a/README.md b/README.md
index 6f57faa09..a0a4eec38 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ Maven
     <dependency>
         <groupId>org.apache.cayenne</groupId>
         <artifactId>cayenne-server</artifactId>
-        <version>5.0-M1</version>
+        <version>5.0-M2</version>
     </dependency>
 </dependencies>
 ```
@@ -69,9 +69,8 @@ compile cayenne.dependency('server')
 ```java
 CayenneRuntime cayenneRuntime = CayenneRuntime.builder()
     .addConfig("cayenne-demo.xml")
-    .dataSource(DataSourceBuilder
-             .url("jdbc:mysql://localhost:3306/cayenne_demo")
-             .driver("com.mysql.cj.jdbc.Driver")
+    .dataSource(CayenneDataSource
+             .of("jdbc:mysql://localhost:3306/cayenne_demo")
              .userName("username")
              .password("password")
              .build())
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 0361549fa..17bd960ef 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -29,6 +29,7 @@ CAY-2978 AI skill: "cayenne-model-naming"
 CAY-2979 AI skill: "cayenne-full-db-sync"
 CAY-2980 Improve model name generation
 CAY-2982 Modeler: when upgrading projects allow upgraders to send outcome 
messages
+CAY-2983 CayenneDataSource: Public-facing DataSource builder
 
 Bug Fixes:
 
diff --git 
a/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/DbGenerateTask.java
 
b/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/DbGenerateTask.java
index 5306bdeb5..8100fff88 100644
--- 
a/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/DbGenerateTask.java
+++ 
b/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/DbGenerateTask.java
@@ -26,7 +26,7 @@ import org.apache.cayenne.configuration.DataNodeDescriptor;
 import org.apache.cayenne.configuration.runtime.DataSourceFactory;
 import org.apache.cayenne.configuration.runtime.DbAdapterFactory;
 import org.apache.cayenne.configuration.runtime.PkGeneratorFactoryProvider;
-import org.apache.cayenne.datasource.DataSourceBuilder;
+import org.apache.cayenne.datasource.CayenneDataSource;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.JdbcAdapter;
 import org.apache.cayenne.dba.PkGenerator;
@@ -165,8 +165,8 @@ public class DbGenerateTask extends BaseCayenneTask {
     }
 
     DataSource createDataSource() {
-        return DataSourceBuilder.url(dataSource.getUrl())
-                .driver(dataSource.getDriver())
+        return CayenneDataSource.of(dataSource.getUrl())
+                .driverClass(dataSource.getDriver())
                 .userName(dataSource.getUsername())
                 .password(dataSource.getPassword())
                 .build();
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/configuration/DefaultRuntimeProperties.java
 
b/cayenne/src/main/java/org/apache/cayenne/configuration/DefaultRuntimeProperties.java
index 62daa73c4..2edfdf6be 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/configuration/DefaultRuntimeProperties.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/configuration/DefaultRuntimeProperties.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.configuration;
 
+import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.cayenne.di.Inject;
@@ -98,4 +99,16 @@ public class DefaultRuntimeProperties implements 
RuntimeProperties {
         String string = get(key);
         return string != null ? "true".equalsIgnoreCase(string) : defaultValue;
     }
+
+    @Override
+    public Map<String, String> toMap() {
+        Map<String, String> map = new HashMap<>(properties);
+
+        // system properties take precedence, matching the "get" behavior
+        for (String name : System.getProperties().stringPropertyNames()) {
+            map.put(name, System.getProperty(name));
+        }
+
+        return map;
+    }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/configuration/RuntimeProperties.java 
b/cayenne/src/main/java/org/apache/cayenne/configuration/RuntimeProperties.java
index dba18b255..363f5649e 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/configuration/RuntimeProperties.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/configuration/RuntimeProperties.java
@@ -18,9 +18,11 @@
  ****************************************************************/
 package org.apache.cayenne.configuration;
 
+import java.util.Map;
+
 /**
  * Represents a properties map for a given {@link 
org.apache.cayenne.runtime.CayenneRuntime}.
- * 
+ *
  * @since 3.1
  */
 public interface RuntimeProperties {
@@ -43,4 +45,11 @@ public interface RuntimeProperties {
     long getLong(String key, long defaultValue);
 
     boolean getBoolean(String key, boolean defaultValue);
+
+    /**
+     * Returns a snapshot of the properties as a map, with the same key 
resolution rules as {@link #get(String)}.
+     *
+     * @since 5.0
+     */
+    Map<String, String> toMap();
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/PropertyDataSourceFactory.java
 
b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/PropertyDataSourceFactory.java
index 622dd1ed6..aced37b81 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/PropertyDataSourceFactory.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/PropertyDataSourceFactory.java
@@ -18,32 +18,27 @@
  ****************************************************************/
 package org.apache.cayenne.configuration.runtime;
 
-import org.apache.cayenne.ConfigurationException;
-import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.DataNodeDescriptor;
 import org.apache.cayenne.configuration.RuntimeProperties;
-import org.apache.cayenne.datasource.DataSourceBuilder;
-import org.apache.cayenne.datasource.UnmanagedPoolingDataSource;
-import org.apache.cayenne.di.AdhocObjectFactory;
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.datasource.CayenneDataSource;
 
 import javax.sql.DataSource;
-import java.sql.Driver;
 
 /**
- * A DataSourceFactrory that creates a DataSource based on system properties.
- * Properties can be set per domain/node name or globally, applying to all 
nodes
- * without explicit property set. The following properties are supported:
+ * A DataSourceFactory that creates a DataSource based on runtime properties. 
Properties can be set per domain/node
+ * name or globally, applying to all nodes without explicit property set. The 
following properties are supported:
  * <ul>
- * <li>cayenne.jdbc.driver[.domain_name.node_name]
  * <li>cayenne.jdbc.url[.domain_name.node_name]
+ * <li>cayenne.jdbc.driver[.domain_name.node_name]
  * <li>cayenne.jdbc.username[.domain_name.node_name]
  * <li>cayenne.jdbc.password[.domain_name.node_name]
- * <li>cayenne.jdbc.min.connections[.domain_name.node_name]
- * <li>cayenne.jdbc.max.conections[.domain_name.node_name]
+ * <li>cayenne.jdbc.min_connections[.domain_name.node_name]
+ * <li>cayenne.jdbc.max_connections[.domain_name.node_name]
+ * <li>cayenne.jdbc.max_wait[.domain_name.node_name]
+ * <li>cayenne.jdbc.validation_query[.domain_name.node_name]
  * </ul>
- * At least url and driver properties must be specified for this factory to
- * return a valid DataSource.
+ * The URL property is required. Pooling is enabled if at least one of the 
connections count properties is set.
  *
  * @since 3.1
  */
@@ -52,51 +47,9 @@ public class PropertyDataSourceFactory implements 
DataSourceFactory {
     @Inject
     protected RuntimeProperties properties;
 
-    @Inject
-    private AdhocObjectFactory objectFactory;
-
     @Override
     public DataSource getDataSource(DataNodeDescriptor nodeDescriptor) {
-
-        String suffix = "." + 
nodeDescriptor.getDataChannelDescriptor().getName() + "." + 
nodeDescriptor.getName();
-
-        String driverClass = getProperty(Constants.JDBC_DRIVER_PROPERTY, 
suffix);
-        String url = getProperty(Constants.JDBC_URL_PROPERTY, suffix);
-        String username = getProperty(Constants.JDBC_USERNAME_PROPERTY, 
suffix);
-        String password = getProperty(Constants.JDBC_PASSWORD_PROPERTY, 
suffix);
-        int minConnections = 
getIntProperty(Constants.JDBC_MIN_CONNECTIONS_PROPERTY, suffix, 1);
-        int maxConnections = 
getIntProperty(Constants.JDBC_MAX_CONNECTIONS_PROPERTY, suffix, 1);
-        long maxQueueWaitTime = 
properties.getLong(Constants.JDBC_MAX_QUEUE_WAIT_TIME,
-                UnmanagedPoolingDataSource.MAX_QUEUE_WAIT_DEFAULT);
-        String validationQuery = 
properties.get(Constants.JDBC_VALIDATION_QUERY_PROPERTY);
-
-        Driver driver = objectFactory.newInstance(Driver.class, driverClass, 
true);
-        return DataSourceBuilder
-                .url(url)
-                .driver(driver)
-                .userName(username)
-                .password(password)
-                .pool(minConnections, maxConnections)
-                .maxQueueWaitTime(maxQueueWaitTime)
-                .validationQuery(validationQuery).build();
-    }
-
-    protected int getIntProperty(String propertyName, String suffix, int 
defaultValue) {
-        String string = getProperty(propertyName, suffix);
-
-        if (string == null) {
-            return defaultValue;
-        }
-
-        try {
-            return Integer.parseInt(string);
-        } catch (NumberFormatException e) {
-            throw new ConfigurationException("Invalid int property '%s': 
'%s'", propertyName, string);
-        }
-    }
-
-    protected String getProperty(String propertyName, String suffix) {
-        String value = properties.get(propertyName + suffix);
-        return value != null ? value : properties.get(propertyName);
+        String nodeName = nodeDescriptor.getDataChannelDescriptor().getName() 
+ "." + nodeDescriptor.getName();
+        return CayenneDataSource.fromProperties(properties.toMap(), 
nodeName).build();
     }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/XMLPoolingDataSourceFactory.java
 
b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/XMLPoolingDataSourceFactory.java
index 7af3c97e5..9ef95f18c 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/XMLPoolingDataSourceFactory.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/XMLPoolingDataSourceFactory.java
@@ -23,15 +23,13 @@ import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.DataNodeDescriptor;
 import org.apache.cayenne.configuration.DataSourceDescriptor;
 import org.apache.cayenne.configuration.RuntimeProperties;
-import org.apache.cayenne.datasource.DataSourceBuilder;
 import org.apache.cayenne.datasource.UnmanagedPoolingDataSource;
-import org.apache.cayenne.di.AdhocObjectFactory;
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.datasource.CayenneDataSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.sql.DataSource;
-import java.sql.Driver;
 
 /**
  * A {@link DataSourceFactory} that loads JDBC connection information from an
@@ -48,9 +46,6 @@ public class XMLPoolingDataSourceFactory implements 
DataSourceFactory {
     @Inject
     private RuntimeProperties properties;
 
-    @Inject
-    private AdhocObjectFactory objectFactory;
-
     @Override
     public DataSource getDataSource(DataNodeDescriptor nodeDescriptor) {
 
@@ -64,10 +59,8 @@ public class XMLPoolingDataSourceFactory implements 
DataSourceFactory {
         long maxQueueWaitTime = properties
                 .getLong(Constants.JDBC_MAX_QUEUE_WAIT_TIME, 
UnmanagedPoolingDataSource.MAX_QUEUE_WAIT_DEFAULT);
 
-        Driver driver = objectFactory.newInstance(Driver.class, 
descriptor.getJdbcDriver(), true);
-
-        return DataSourceBuilder.url(descriptor.getDataSourceUrl())
-                .driver(driver)
+        return CayenneDataSource.of(descriptor.getDataSourceUrl())
+                .driverClass(descriptor.getJdbcDriver())
                 .userName(descriptor.getUserName())
                 .password(descriptor.getPassword())
                 .pool(descriptor.getMinConnections(), 
descriptor.getMaxConnections())
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/datasource/CayenneDataSource.java 
b/cayenne/src/main/java/org/apache/cayenne/datasource/CayenneDataSource.java
new file mode 100644
index 000000000..1911f098d
--- /dev/null
+++ b/cayenne/src/main/java/org/apache/cayenne/datasource/CayenneDataSource.java
@@ -0,0 +1,301 @@
+/*****************************************************************
+ *   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.apache.cayenne.datasource;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.configuration.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * An entry point for manually building DataSources. Produces instances of 
Cayenne own DataSource. Alternatively, you
+ * can use any JDBC-compliant third-party DataSources with Cayenne.
+ *
+ * @since 5.0
+ */
+public class CayenneDataSource {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(CayenneDataSource.class);
+
+    /**
+     * Starts building a DataSource for the given database URL.
+     */
+    public static Builder of(String url) {
+        return new Builder(url);
+    }
+
+    /**
+     * Starts building a DataSource with settings taken from the following map 
keys:
+     * <ul>
+     * <li>cayenne.jdbc.url
+     * <li>cayenne.jdbc.driver
+     * <li>cayenne.jdbc.username
+     * <li>cayenne.jdbc.password
+     * <li>cayenne.jdbc.min_connections
+     * <li>cayenne.jdbc.max_connections
+     * <li>cayenne.jdbc.max_wait
+     * <li>cayenne.jdbc.validation_query
+     * </ul>
+     * The URL property is required. Pooling is enabled if at least one of the 
connections count properties is set.
+     * Within the Cayenne stack, use {@code RuntimeProperties.toMap()} to pass 
the runtime properties here.
+     */
+    public static Builder fromProperties(Map<String, String> properties) {
+        Objects.requireNonNull(properties, "Null 'properties'");
+        return fromResolvedProperties(properties, "");
+    }
+
+    /**
+     * Starts building a DataSource with settings taken from a properties map, 
same as {@link #fromProperties(Map)},
+     * but first checking properties with a ".nodeName" suffix (e.g. 
"cayenne.jdbc.url.mynode"), and falling back to
+     * the unsuffixed ones.
+     */
+    public static Builder fromProperties(Map<String, String> properties, 
String nodeName) {
+        Objects.requireNonNull(properties, "Null 'properties'");
+        Objects.requireNonNull(nodeName, "Null 'nodeName'");
+        return fromResolvedProperties(properties, "." + nodeName);
+    }
+
+    private static Builder fromResolvedProperties(Map<String, String> props, 
String suffix) {
+
+        String url = prop(props, Constants.JDBC_URL_PROPERTY, suffix);
+        if (url == null) {
+            throw new CayenneRuntimeException("Missing DataSource URL property 
'%s%s'",
+                    Constants.JDBC_URL_PROPERTY,
+                    suffix);
+        }
+
+        Builder builder = new Builder(url)
+                .userName(prop(props, Constants.JDBC_USERNAME_PROPERTY, 
suffix))
+                .password(prop(props, Constants.JDBC_PASSWORD_PROPERTY, 
suffix));
+
+        String driverClassName = prop(props, Constants.JDBC_DRIVER_PROPERTY, 
suffix);
+        if (driverClassName != null) {
+            builder.driverClass(driverClassName);
+        }
+
+        int minConnections = intProp(props, 
Constants.JDBC_MIN_CONNECTIONS_PROPERTY, suffix, -1);
+        int maxConnections = intProp(props, 
Constants.JDBC_MAX_CONNECTIONS_PROPERTY, suffix, -1);
+        if (minConnections >= 0 || maxConnections >= 0) {
+            int min = minConnections >= 0 ? minConnections : 1;
+            builder.pool(min, maxConnections >= 0 ? maxConnections : 
Math.max(min, 1));
+        }
+
+        long maxQueueWaitTime = longProp(props, 
Constants.JDBC_MAX_QUEUE_WAIT_TIME, suffix, -1);
+        if (maxQueueWaitTime >= 0) {
+            builder.maxQueueWaitTime(maxQueueWaitTime);
+        }
+
+        String validationQuery = prop(props, 
Constants.JDBC_VALIDATION_QUERY_PROPERTY, suffix);
+        if (validationQuery != null) {
+            builder.validationQuery(validationQuery);
+        }
+
+        return builder;
+    }
+
+    private static String prop(Map<String, String> props, String name, String 
suffix) {
+        // fallback to default property shared by all data nodes
+        String value = props.get(name + suffix);
+        return value != null ? value : props.get(name);
+    }
+
+    private static int intProp(Map<String, String> props, String name, String 
suffix, int defaultValue) {
+        String value = prop(props, name, suffix);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    private static long longProp(Map<String, String> props, String name, 
String suffix, long defaultValue) {
+        String value = prop(props, name, suffix);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        try {
+            return Long.parseLong(value);
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    private CayenneDataSource() {
+    }
+
+    public static class Builder {
+
+        private final String url;
+        private String userName;
+        private String password;
+        private String driverClass;
+
+        private Integer minConnections;
+        private Integer maxConnections;
+        private Long maxQueueWaitTime;
+        private String validationQuery;
+
+        private Builder(String url) {
+            this.url = Objects.requireNonNull(url, "Null 'url'");
+        }
+
+        public Builder userName(String userName) {
+            this.userName = userName;
+            return this;
+        }
+
+        public Builder password(String password) {
+            this.password = password;
+            return this;
+        }
+
+        /**
+         * Sets a class name of the JDBC driver. This i optional and is only 
used in special circumstances. Normally,
+         * JDBC-compliant drivers are discovered automatically, and resolved 
based on the URL.
+         */
+        public Builder driverClass(String driverClassName) {
+            this.driverClass = driverClassName;
+            return this;
+        }
+
+        /**
+         * Turns the built DataSource into a connection pool with the given 
connection count bounds.
+         */
+        public Builder pool(int minConnections, int maxConnections) {
+
+            if (minConnections < 0) {
+                throw new CayenneRuntimeException("Minimum number of 
connections can not be negative (%d)", minConnections);
+            }
+
+            if (maxConnections < 0) {
+                throw new CayenneRuntimeException("Maximum number of 
connections can not be negative (%d)", maxConnections);
+            }
+
+            if (minConnections > maxConnections) {
+                throw new CayenneRuntimeException("Minimum number of 
connections can not be bigger than maximum.");
+            }
+
+            this.minConnections = minConnections;
+            this.maxConnections = maxConnections;
+            return this;
+        }
+
+        /**
+         * Sets a maximum time in milliseconds a connection request may wait 
for a free connection in the pool. Ignored
+         * unless {@link #pool(int, int)} is also called.
+         */
+        public Builder maxQueueWaitTime(long maxQueueWaitTime) {
+            this.maxQueueWaitTime = maxQueueWaitTime;
+            return this;
+        }
+
+        /**
+         * Sets a SQL query used by the pool to check connection health. 
Ignored unless {@link #pool(int, int)} is also
+         * called.
+         */
+        public Builder validationQuery(String validationQuery) {
+            this.validationQuery = validationQuery;
+            return this;
+        }
+
+        /**
+         * Builds a DataSource that is pooling if {@link #pool(int, int)} was 
called, and non-pooling otherwise. A
+         * pooling DataSource implements {@link PoolingDataSource} and must be 
explicitly closed by the caller when no
+         * longer in use.
+         */
+        public DataSource build() {
+
+            if (minConnections == null) {
+                if (maxQueueWaitTime != null) {
+                    LOGGER.warn("'maxQueueWaitTime' is ignored for a 
non-pooling DataSource. Call 'pool(min, max)' to enable pooling.");
+                }
+
+                if (validationQuery != null) {
+                    LOGGER.warn("'validationQuery' is ignored for a 
non-pooling DataSource. Call 'pool(min, max)' to enable pooling.");
+                }
+            }
+
+            DataSource nonPooling = new DriverDataSource(loadDriver(), url, 
userName, password);
+            return minConnections != null ? pool(nonPooling) : nonPooling;
+        }
+
+        private PoolingDataSource pool(DataSource nonPooling) {
+
+            PoolingDataSourceParameters parameters = new 
PoolingDataSourceParameters();
+            parameters.setMinConnections(minConnections);
+            parameters.setMaxConnections(maxConnections);
+            parameters.setMaxQueueWaitTime(
+                    maxQueueWaitTime != null ? maxQueueWaitTime : 
UnmanagedPoolingDataSource.MAX_QUEUE_WAIT_DEFAULT);
+            parameters.setValidationQuery(validationQuery);
+
+            return new ManagedPoolingDataSource(new 
UnmanagedPoolingDataSource(nonPooling, parameters));
+        }
+
+        private Driver loadDriver() {
+
+            if (driverClass == null) {
+                try {
+                    return DriverManager.getDriver(url);
+                } catch (SQLException ex) {
+                    throw new CayenneRuntimeException("No registered JDBC 
driver accepting the URL '%s': %s",
+                            url,
+                            ex.getMessage());
+                }
+            }
+
+            return DriverManager.drivers()
+                    .filter(d -> d.getClass().getName().equals(driverClass))
+                    .findFirst()
+                    .orElseGet(this::instantiateDriver);
+        }
+
+        private Driver instantiateDriver() {
+
+            Class<?> driverClass;
+            try {
+                // note: implicitly using current class's ClassLoader ....
+                driverClass = Class.forName(this.driverClass);
+            } catch (Exception ex) {
+                throw new CayenneRuntimeException("Can not load JDBC driver 
named '%s': %s",
+                        this.driverClass,
+                        ex.getMessage());
+            }
+
+            try {
+                return (Driver) 
driverClass.getDeclaredConstructor().newInstance();
+            } catch (Exception ex) {
+                throw new CayenneRuntimeException("Error instantiating driver 
'%s': %s",
+                        this.driverClass,
+                        ex.getMessage());
+            }
+        }
+    }
+}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/datasource/DataSourceBuilder.java 
b/cayenne/src/main/java/org/apache/cayenne/datasource/DataSourceBuilder.java
index 77c3731d4..b00724cab 100644
--- a/cayenne/src/main/java/org/apache/cayenne/datasource/DataSourceBuilder.java
+++ b/cayenne/src/main/java/org/apache/cayenne/datasource/DataSourceBuilder.java
@@ -26,9 +26,11 @@ import java.sql.Driver;
 /**
  * A builder class that allows to build a {@link DataSource} with optional
  * pooling.
- * 
+ *
  * @since 4.0
+ * @deprecated in favor of {@link CayenneDataSource}
  */
+@Deprecated(since = "5.0", forRemoval = true)
 public class DataSourceBuilder {
 
        private String userName;
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/datasource/DriverDataSource.java 
b/cayenne/src/main/java/org/apache/cayenne/datasource/DriverDataSource.java
index be9fb125f..335c9fa9c 100644
--- a/cayenne/src/main/java/org/apache/cayenne/datasource/DriverDataSource.java
+++ b/cayenne/src/main/java/org/apache/cayenne/datasource/DriverDataSource.java
@@ -19,151 +19,143 @@
 
 package org.apache.cayenne.datasource;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
 import java.io.PrintWriter;
 import java.sql.Connection;
 import java.sql.Driver;
 import java.sql.DriverManager;
 import java.sql.SQLException;
-import java.sql.SQLFeatureNotSupportedException;
+import java.util.Objects;
 import java.util.Properties;
 
-import javax.sql.DataSource;
-
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.util.Util;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 /**
  * A non-pooling DataSource implementation wrapping a JDBC driver.
  */
 public class DriverDataSource implements DataSource {
 
-       private static final Logger LOGGER = 
LoggerFactory.getLogger(DriverDataSource.class);
-
-       protected Driver driver;
-       protected String connectionUrl;
-       protected String userName;
-       protected String password;
-
-       /**
-        * Creates a DriverDataSource wrapping a given Driver. If "driver" is 
null,
-        * DriverDataSource will consult DriverManager for a registered driver 
for
-        * the given URL. So when specifying null, a user must take care of
-        * registering the driver. "connectionUrl" on the other hand must NOT be
-        * null.
-        * 
-        * @since 1.1
-        */
-       public DriverDataSource(Driver driver, String connectionUrl, String 
userName, String password) {
-
-               if (connectionUrl == null) {
-                       throw new NullPointerException("Null 'connectionUrl'");
-               }
-
-               this.driver = driver;
-               this.connectionUrl = connectionUrl;
-               this.userName = userName;
-               this.password = password;
-       }
-
-       /**
-        * Returns a new database connection, using preconfigured data to 
locate the
-        * database and obtain a connection.
-        */
-       @Override
-       public Connection getConnection() throws SQLException {
-               // login with internal credentials
-               return getConnection(userName, password);
-       }
-
-       /**
-        * Returns a new database connection using provided credentials to 
login to
-        * the database.
-        */
-       @Override
-       public Connection getConnection(String userName, String password) 
throws SQLException {
-               try {
-
-                       logConnect(connectionUrl, userName, password);
-                       Connection c = null;
-
-                       if (driver == null) {
-                               c = DriverManager.getConnection(connectionUrl, 
userName, password);
-                       } else {
-                               Properties connectProperties = new Properties();
-
-                               if (userName != null) {
-                                       connectProperties.put("user", userName);
-                               }
-
-                               if (password != null) {
-                                       connectProperties.put("password", 
password);
-                               }
-                               c = driver.connect(connectionUrl, 
connectProperties);
-                       }
-
-                       // some drivers (Oracle) return null connections 
instead of throwing
-                       // an exception... fix it here
-
-                       if (c == null) {
-                               throw new SQLException("Can't establish 
connection: " + connectionUrl);
-                       }
-
-                       LOGGER.info("+++ Connecting: SUCCESS.");
-
-                       return c;
-               } catch (SQLException ex) {
-                       LOGGER.info("*** Connecting: FAILURE.", ex);
-                       throw ex;
-               }
-       }
-
-       private void logConnect(String url, String userName, String password) {
-               LOGGER.info("Connecting to '{}' as '{}'", url, userName);
-       }
-
-       @Override
-       public int getLoginTimeout() throws SQLException {
-               return -1;
-       }
-
-       @Override
-       public void setLoginTimeout(int seconds) throws SQLException {
-               // noop
-       }
-
-       @Override
-       public PrintWriter getLogWriter() throws SQLException {
-               return DriverManager.getLogWriter();
-       }
-
-       @Override
-       public void setLogWriter(PrintWriter out) throws SQLException {
-               DriverManager.setLogWriter(out);
-       }
-
-       /**
-        * @since 3.0
-        */
-       @Override
-       public boolean isWrapperFor(Class<?> iface) throws SQLException {
-               throw new UnsupportedOperationException();
-       }
-
-       /**
-        * @since 3.0
-        */
-       @Override
-       public <T> T unwrap(Class<T> iface) throws SQLException {
-               throw new UnsupportedOperationException();
-       }
-
-       /**
-        * @since 3.1
-        */
-       @Override
-       public java.util.logging.Logger getParentLogger() throws 
SQLFeatureNotSupportedException {
-               throw new UnsupportedOperationException();
-       }
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(DriverDataSource.class);
+
+    protected final Driver driver;
+    protected final String connectionUrl;
+    protected final String userName;
+    protected final String password;
+
+    /**
+     * Creates a DriverDataSource wrapping a given Driver. If "driver" is null,
+     * DriverDataSource will consult DriverManager for a registered driver for
+     * the given URL. So when specifying null, a user must take care of
+     * registering the driver. "connectionUrl" on the other hand must NOT be
+     * null.
+     *
+     * @since 1.1
+     */
+    public DriverDataSource(Driver driver, String connectionUrl, String 
userName, String password) {
+        this.driver = driver;
+        this.connectionUrl = Objects.requireNonNull(connectionUrl, "Null 
'connectionUrl'");
+        this.userName = userName;
+        this.password = password;
+    }
+
+    /**
+     * Returns a new database connection, using preconfigured data to locate 
the
+     * database and obtain a connection.
+     */
+    @Override
+    public Connection getConnection() throws SQLException {
+        // login with internal credentials
+        return getConnection(userName, password);
+    }
+
+    /**
+     * Returns a new database connection using provided credentials to login to
+     * the database.
+     */
+    @Override
+    public Connection getConnection(String userName, String password) throws 
SQLException {
+        try {
+
+            logConnect(connectionUrl, userName);
+            Connection c;
+
+            if (driver == null) {
+                c = DriverManager.getConnection(connectionUrl, userName, 
password);
+            } else {
+                Properties connectProperties = new Properties();
+
+                if (userName != null) {
+                    connectProperties.put("user", userName);
+                }
+
+                if (password != null) {
+                    connectProperties.put("password", password);
+                }
+                c = driver.connect(connectionUrl, connectProperties);
+            }
+
+            // some drivers (Oracle) return null connections instead of 
throwing
+            // an exception... fix it here
+
+            if (c == null) {
+                throw new SQLException("Can't establish connection: " + 
connectionUrl);
+            }
+
+            LOGGER.info("+++ Connecting: SUCCESS.");
+
+            return c;
+        } catch (SQLException ex) {
+            LOGGER.info("*** Connecting: FAILURE.", ex);
+            throw ex;
+        }
+    }
+
+    private void logConnect(String url, String userName) {
+        LOGGER.info("Connecting to '{}' as '{}'", url, userName);
+    }
+
+    @Override
+    public int getLoginTimeout() {
+        return -1;
+    }
+
+    @Override
+    public void setLoginTimeout(int seconds) {
+        // noop
+    }
+
+    @Override
+    public PrintWriter getLogWriter() {
+        return DriverManager.getLogWriter();
+    }
+
+    @Override
+    public void setLogWriter(PrintWriter out) {
+        DriverManager.setLogWriter(out);
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public boolean isWrapperFor(Class<?> iface) throws SQLException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public <T> T unwrap(Class<T> iface) throws SQLException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public java.util.logging.Logger getParentLogger() {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java
 
b/cayenne/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java
index 800819dc9..6237fcbb7 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java
@@ -18,16 +18,15 @@
  ****************************************************************/
 package org.apache.cayenne.datasource;
 
+import org.apache.cayenne.di.ScopeEventListener;
+
+import javax.sql.DataSource;
 import java.io.PrintWriter;
 import java.sql.Connection;
 import java.sql.SQLException;
 import java.sql.SQLFeatureNotSupportedException;
 import java.util.logging.Logger;
 
-import javax.sql.DataSource;
-
-import org.apache.cayenne.di.ScopeEventListener;
-
 /**
  * A wrapper for {@link UnmanagedPoolingDataSource} that automatically manages
  * the underlying connection pool size.
@@ -58,10 +57,6 @@ public class ManagedPoolingDataSource implements 
PoolingDataSource, ScopeEventLi
        int poolSize() {
                return dataSourceManager.getDataSource().poolSize();
        }
-
-       int availableSize() {
-               return dataSourceManager.getDataSource().availableSize();
-       }
        
        int canExpandSize() {
                return dataSourceManager.getDataSource().canExpandSize();
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolAwareConnection.java 
b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolAwareConnection.java
index 388d50298..42966f110 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolAwareConnection.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolAwareConnection.java
@@ -39,546 +39,545 @@ import java.util.Properties;
 import java.util.concurrent.Executor;
 
 /**
- * A {@link Connection} wrapper that interacts with the
- * {@link UnmanagedPoolingDataSource}, allowing to recycle connections and 
track
+ * A Connection wrapper that interacts with the UnmanagedPoolingDataSource, 
allowing to recycle connections and track
  * failures.
- * 
+ *
  * @since 4.0
  */
 public class PoolAwareConnection implements Connection {
 
-       private UnmanagedPoolingDataSource parent;
-       private Connection connection;
-       private String validationQuery;
-
-       public PoolAwareConnection(UnmanagedPoolingDataSource parent, 
Connection connection, String validationQuery) {
-               this.parent = parent;
-               this.connection = connection;
-               this.validationQuery = validationQuery;
-       }
-
-       Connection getConnection() {
-               return connection;
-       }
-
-       boolean validate() {
-
-               if (validationQuery == null) {
-                       return true;
-               }
-
-               try {
-
-                       try (Statement statement = 
connection.createStatement();) {
-
-                               try (ResultSet rs = 
statement.executeQuery(validationQuery);) {
-
-                                       if (!rs.next()) {
-                                               throw new 
SQLException("Connection validation failed, no result for query: " + 
validationQuery);
-                                       }
-                               }
-                       }
-               } catch (SQLException e) {
-                       return false;
-               }
-
-               return true;
-       }
-
-       void recover(SQLException reconnectCause) throws SQLException {
-
-               try {
-                       connection.close();
-               } catch (SQLException e) {
-                       // ignore exception, since connection is expected to be 
in a bad
-                       // state
-               }
-
-               // TODO: autocommit, tx isolation, and other connection 
settings may
-               // change when resetting connection and need to be restored...
-               try {
-                       connection = parent.createUnwrapped();
-               } catch (SQLException e) {
-                       parent.retire(this);
-                       throw reconnectCause;
-               }
-       }
-
-       @Override
-       public void clearWarnings() throws SQLException {
-               try {
-                       connection.clearWarnings();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public void close() throws SQLException {
-               parent.reclaim(this);
-       }
-
-       @Override
-       public void commit() throws SQLException {
-               try {
-                       connection.commit();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public Statement createStatement() throws SQLException {
-               try {
-                       return connection.createStatement();
-               } catch (SQLException sqlEx) {
-                       recover(sqlEx);
-                       return connection.createStatement();
-               }
-       }
-
-       @Override
-       public Statement createStatement(int resultSetType, int 
resultSetConcurrency) throws SQLException {
-               try {
-                       return connection.createStatement(resultSetType, 
resultSetConcurrency);
-               } catch (SQLException e) {
-                       recover(e);
-                       return connection.createStatement(resultSetType, 
resultSetConcurrency);
-               }
-       }
-
-       @Override
-       public boolean getAutoCommit() throws SQLException {
-               try {
-                       return connection.getAutoCommit();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public String getCatalog() throws SQLException {
-               try {
-                       return connection.getCatalog();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public DatabaseMetaData getMetaData() throws SQLException {
-               try {
-                       return connection.getMetaData();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public int getTransactionIsolation() throws SQLException {
-               try {
-                       return connection.getTransactionIsolation();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public SQLWarning getWarnings() throws SQLException {
-               try {
-                       return connection.getWarnings();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public boolean isClosed() throws SQLException {
-
-               try {
-                       return connection.isClosed();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public boolean isReadOnly() throws SQLException {
-               try {
-                       return connection.isReadOnly();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public String nativeSQL(String sql) throws SQLException {
-               try {
-                       return connection.nativeSQL(sql);
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public CallableStatement prepareCall(String sql) throws SQLException {
-               try {
-                       return connection.prepareCall(sql);
-               } catch (SQLException sqlEx) {
-                       recover(sqlEx);
-                       return connection.prepareCall(sql);
-               }
-       }
-
-       @Override
-       public CallableStatement prepareCall(String sql, int resultSetType, int 
resultSetConcurrency) throws SQLException {
-               try {
-                       return connection.prepareCall(sql, resultSetType, 
resultSetConcurrency);
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.prepareCall(sql, resultSetType, 
resultSetConcurrency);
-               }
-       }
-
-       @Override
-       public PreparedStatement prepareStatement(String sql) throws 
SQLException {
-               try {
-                       return connection.prepareStatement(sql);
-               } catch (SQLException sqlEx) {
-                       recover(sqlEx);
-                       return connection.prepareStatement(sql);
-               }
-       }
-
-       @Override
-       public PreparedStatement prepareStatement(String sql, int 
resultSetType, int resultSetConcurrency)
-                       throws SQLException {
-               try {
-                       return connection.prepareStatement(sql, resultSetType, 
resultSetConcurrency);
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.prepareStatement(sql, resultSetType, 
resultSetConcurrency);
-               }
-       }
-
-       @Override
-       public void rollback() throws SQLException {
-               try {
-                       connection.rollback();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public void setAutoCommit(boolean autoCommit) throws SQLException {
-               try {
-                       connection.setAutoCommit(autoCommit);
-               } catch (SQLException sqlEx) {
-
-                       try {
-                               
UnmanagedPoolingDataSource.sybaseAutoCommitPatch(connection, sqlEx, autoCommit);
-                       } catch (SQLException patchEx) {
-                               parent.retire(this);
-                               throw sqlEx;
-                       }
-               }
-       }
-
-       @Override
-       public void setCatalog(String catalog) throws SQLException {
-               try {
-                       connection.setCatalog(catalog);
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public void setReadOnly(boolean readOnly) throws SQLException {
-               try {
-                       connection.setReadOnly(readOnly);
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public void setTransactionIsolation(int level) throws SQLException {
-               try {
-                       connection.setTransactionIsolation(level);
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public Map<String, Class<?>> getTypeMap() throws SQLException {
-               try {
-                       return connection.getTypeMap();
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
-               try {
-                       connection.setTypeMap(map);
-               } catch (SQLException sqlEx) {
-                       parent.retire(this);
-                       throw sqlEx;
-               }
-       }
-
-       @Override
-       public void setHoldability(int holdability) throws SQLException {
-               throw new java.lang.UnsupportedOperationException("Method 
setHoldability() not yet implemented.");
-       }
-
-       @Override
-       public int getHoldability() throws SQLException {
-               throw new java.lang.UnsupportedOperationException("Method 
getHoldability() not yet implemented.");
-       }
-
-       @Override
-       public Savepoint setSavepoint() throws SQLException {
-               throw new java.lang.UnsupportedOperationException("Method 
setSavepoint() not yet implemented.");
-       }
-
-       @Override
-       public Savepoint setSavepoint(String name) throws SQLException {
-               throw new java.lang.UnsupportedOperationException("Method 
setSavepoint() not yet implemented.");
-       }
-
-       @Override
-       public void rollback(Savepoint savepoint) throws SQLException {
-               throw new java.lang.UnsupportedOperationException("Method 
rollback() not yet implemented.");
-       }
-
-       @Override
-       public void releaseSavepoint(Savepoint savepoint) throws SQLException {
-               throw new java.lang.UnsupportedOperationException("Method 
releaseSavepoint() not yet implemented.");
-       }
-
-       @Override
-       public Statement createStatement(int resultSetType, int 
resultSetConcurrency, int resultSetHoldability)
-                       throws SQLException {
-               throw new java.lang.UnsupportedOperationException("Method 
createStatement() not yet implemented.");
-       }
-
-       @Override
-       public PreparedStatement prepareStatement(String sql, int 
resultSetType, int resultSetConcurrency,
-                       int resultSetHoldability) throws SQLException {
-               throw new java.lang.UnsupportedOperationException("Method 
prepareStatement() not yet implemented.");
-       }
-
-       @Override
-       public CallableStatement prepareCall(String sql, int resultSetType, int 
resultSetConcurrency,
-                       int resultSetHoldability) throws SQLException {
-               try {
-                       return connection.prepareCall(sql, resultSetType, 
resultSetConcurrency, resultSetHoldability);
-               } catch (SQLException e) {
-
-                       recover(e);
-                       return connection.prepareCall(sql, resultSetType, 
resultSetConcurrency, resultSetHoldability);
-               }
-       }
-
-       @Override
-       public PreparedStatement prepareStatement(String sql, int 
autoGeneratedKeys) throws SQLException {
-
-               try {
-                       return connection.prepareStatement(sql, 
autoGeneratedKeys);
-               } catch (SQLException e) {
-
-                       recover(e);
-                       return connection.prepareStatement(sql, 
autoGeneratedKeys);
-               }
-       }
-
-       @Override
-       public PreparedStatement prepareStatement(String sql, int[] 
columnIndexes) throws SQLException {
-               try {
-                       return connection.prepareStatement(sql, columnIndexes);
-               } catch (SQLException e) {
-
-                       recover(e);
-                       return connection.prepareStatement(sql, columnIndexes);
-               }
-       }
-
-       @Override
-       public PreparedStatement prepareStatement(String sql, String[] 
columnNames) throws SQLException {
-               try {
-                       return connection.prepareStatement(sql, columnNames);
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.prepareStatement(sql, columnNames);
-               }
-       }
-
-       @Override
-       public Array createArrayOf(String typeName, Object[] elements) throws 
SQLException {
-               try {
-                       return connection.createArrayOf(typeName, elements);
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.createArrayOf(typeName, elements);
-               }
-       }
-
-       @Override
-       public Blob createBlob() throws SQLException {
-               try {
-                       return connection.createBlob();
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.createBlob();
-               }
-       }
-
-       @Override
-       public Clob createClob() throws SQLException {
-               try {
-                       return connection.createClob();
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.createClob();
-               }
-       }
-
-       @Override
-       public Struct createStruct(String typeName, Object[] attributes) throws 
SQLException {
-               try {
-                       return connection.createStruct(typeName, attributes);
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.createStruct(typeName, attributes);
-               }
-       }
-
-       @Override
-       public Properties getClientInfo() throws SQLException {
-               try {
-                       return connection.getClientInfo();
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.getClientInfo();
-               }
-       }
-
-       @Override
-       public String getClientInfo(String name) throws SQLException {
-               try {
-                       return connection.getClientInfo(name);
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.getClientInfo(name);
-               }
-       }
-
-       @Override
-       public boolean isValid(int timeout) throws SQLException {
-               try {
-                       return connection.isValid(timeout);
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.isValid(timeout);
-               }
-       }
-
-       @Override
-       public boolean isWrapperFor(Class<?> iface) throws SQLException {
-               return (PoolAwareConnection.class.equals(iface)) ? true : 
connection.isWrapperFor(iface);
-       }
-
-       @SuppressWarnings("unchecked")
-       @Override
-       public <T> T unwrap(Class<T> iface) throws SQLException {
-               return PoolAwareConnection.class.equals(iface) ? (T) this : 
connection.unwrap(iface);
-       }
-
-       @Override
-       public NClob createNClob() throws SQLException {
-               try {
-                       return connection.createNClob();
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.createNClob();
-               }
-       }
-
-       @Override
-       public SQLXML createSQLXML() throws SQLException {
-               try {
-                       return connection.createSQLXML();
-               } catch (SQLException sqlEx) {
-
-                       recover(sqlEx);
-                       return connection.createSQLXML();
-               }
-       }
-
-       @Override
-       public void setClientInfo(Properties properties) throws 
SQLClientInfoException {
-               connection.setClientInfo(properties);
-       }
-
-       @Override
-       public void setClientInfo(String name, String value) throws 
SQLClientInfoException {
-               connection.setClientInfo(name, value);
-       }
-
-       @Override
-       public void setSchema(String schema) throws SQLException {
-               connection.setSchema(schema);
-       }
-
-       @Override
-       public String getSchema() throws SQLException {
-               return connection.getSchema();
-       }
-
-       @Override
-       public void abort(Executor executor) throws SQLException {
-               connection.abort(executor);
-       }
-
-       @Override
-       public void setNetworkTimeout(Executor executor, int milliseconds) 
throws SQLException {
-               connection.setNetworkTimeout(executor, milliseconds);
-       }
-
-       @Override
-       public int getNetworkTimeout() throws SQLException {
-               return connection.getNetworkTimeout();
-       }
+    private final UnmanagedPoolingDataSource parent;
+    private Connection connection;
+    private final String validationQuery;
+
+    public PoolAwareConnection(UnmanagedPoolingDataSource parent, Connection 
connection, String validationQuery) {
+        this.parent = parent;
+        this.connection = connection;
+        this.validationQuery = validationQuery;
+    }
+
+    Connection getConnection() {
+        return connection;
+    }
+
+    boolean validate() {
+
+        if (validationQuery == null) {
+            return true;
+        }
+
+        try {
+
+            try (Statement statement = connection.createStatement()) {
+
+                try (ResultSet rs = statement.executeQuery(validationQuery)) {
+
+                    if (!rs.next()) {
+                        throw new SQLException("Connection validation failed, 
no result for query: " + validationQuery);
+                    }
+                }
+            }
+        } catch (SQLException e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    void recover(SQLException reconnectCause) throws SQLException {
+
+        try {
+            connection.close();
+        } catch (SQLException e) {
+            // ignore exception, since connection is expected to be in a bad
+            // state
+        }
+
+        // TODO: autocommit, tx isolation, and other connection settings may
+        // change when resetting connection and need to be restored...
+        try {
+            connection = parent.createUnwrapped();
+        } catch (SQLException e) {
+            parent.retire(this);
+            throw reconnectCause;
+        }
+    }
+
+    @Override
+    public void clearWarnings() throws SQLException {
+        try {
+            connection.clearWarnings();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public void close() throws SQLException {
+        parent.reclaim(this);
+    }
+
+    @Override
+    public void commit() throws SQLException {
+        try {
+            connection.commit();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public Statement createStatement() throws SQLException {
+        try {
+            return connection.createStatement();
+        } catch (SQLException sqlEx) {
+            recover(sqlEx);
+            return connection.createStatement();
+        }
+    }
+
+    @Override
+    public Statement createStatement(int resultSetType, int 
resultSetConcurrency) throws SQLException {
+        try {
+            return connection.createStatement(resultSetType, 
resultSetConcurrency);
+        } catch (SQLException e) {
+            recover(e);
+            return connection.createStatement(resultSetType, 
resultSetConcurrency);
+        }
+    }
+
+    @Override
+    public boolean getAutoCommit() throws SQLException {
+        try {
+            return connection.getAutoCommit();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public String getCatalog() throws SQLException {
+        try {
+            return connection.getCatalog();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public DatabaseMetaData getMetaData() throws SQLException {
+        try {
+            return connection.getMetaData();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public int getTransactionIsolation() throws SQLException {
+        try {
+            return connection.getTransactionIsolation();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public SQLWarning getWarnings() throws SQLException {
+        try {
+            return connection.getWarnings();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public boolean isClosed() throws SQLException {
+
+        try {
+            return connection.isClosed();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public boolean isReadOnly() throws SQLException {
+        try {
+            return connection.isReadOnly();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public String nativeSQL(String sql) throws SQLException {
+        try {
+            return connection.nativeSQL(sql);
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public CallableStatement prepareCall(String sql) throws SQLException {
+        try {
+            return connection.prepareCall(sql);
+        } catch (SQLException sqlEx) {
+            recover(sqlEx);
+            return connection.prepareCall(sql);
+        }
+    }
+
+    @Override
+    public CallableStatement prepareCall(String sql, int resultSetType, int 
resultSetConcurrency) throws SQLException {
+        try {
+            return connection.prepareCall(sql, resultSetType, 
resultSetConcurrency);
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.prepareCall(sql, resultSetType, 
resultSetConcurrency);
+        }
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql) throws SQLException {
+        try {
+            return connection.prepareStatement(sql);
+        } catch (SQLException sqlEx) {
+            recover(sqlEx);
+            return connection.prepareStatement(sql);
+        }
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql, int resultSetType, 
int resultSetConcurrency)
+            throws SQLException {
+        try {
+            return connection.prepareStatement(sql, resultSetType, 
resultSetConcurrency);
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.prepareStatement(sql, resultSetType, 
resultSetConcurrency);
+        }
+    }
+
+    @Override
+    public void rollback() throws SQLException {
+        try {
+            connection.rollback();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public void setAutoCommit(boolean autoCommit) throws SQLException {
+        try {
+            connection.setAutoCommit(autoCommit);
+        } catch (SQLException sqlEx) {
+
+            try {
+                UnmanagedPoolingDataSource.sybaseAutoCommitPatch(connection, 
sqlEx, autoCommit);
+            } catch (SQLException patchEx) {
+                parent.retire(this);
+                throw sqlEx;
+            }
+        }
+    }
+
+    @Override
+    public void setCatalog(String catalog) throws SQLException {
+        try {
+            connection.setCatalog(catalog);
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public void setReadOnly(boolean readOnly) throws SQLException {
+        try {
+            connection.setReadOnly(readOnly);
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public void setTransactionIsolation(int level) throws SQLException {
+        try {
+            connection.setTransactionIsolation(level);
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public Map<String, Class<?>> getTypeMap() throws SQLException {
+        try {
+            return connection.getTypeMap();
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
+        try {
+            connection.setTypeMap(map);
+        } catch (SQLException sqlEx) {
+            parent.retire(this);
+            throw sqlEx;
+        }
+    }
+
+    @Override
+    public void setHoldability(int holdability) {
+        throw new java.lang.UnsupportedOperationException("Method 
setHoldability() not yet implemented.");
+    }
+
+    @Override
+    public int getHoldability() {
+        throw new java.lang.UnsupportedOperationException("Method 
getHoldability() not yet implemented.");
+    }
+
+    @Override
+    public Savepoint setSavepoint() {
+        throw new java.lang.UnsupportedOperationException("Method 
setSavepoint() not yet implemented.");
+    }
+
+    @Override
+    public Savepoint setSavepoint(String name) {
+        throw new java.lang.UnsupportedOperationException("Method 
setSavepoint() not yet implemented.");
+    }
+
+    @Override
+    public void rollback(Savepoint savepoint) throws SQLException {
+        throw new java.lang.UnsupportedOperationException("Method rollback() 
not yet implemented.");
+    }
+
+    @Override
+    public void releaseSavepoint(Savepoint savepoint) {
+        throw new java.lang.UnsupportedOperationException("Method 
releaseSavepoint() not yet implemented.");
+    }
+
+    @Override
+    public Statement createStatement(int resultSetType, int 
resultSetConcurrency, int resultSetHoldability)
+            throws SQLException {
+        throw new java.lang.UnsupportedOperationException("Method 
createStatement() not yet implemented.");
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql, int resultSetType, 
int resultSetConcurrency,
+                                              int resultSetHoldability) throws 
SQLException {
+        throw new java.lang.UnsupportedOperationException("Method 
prepareStatement() not yet implemented.");
+    }
+
+    @Override
+    public CallableStatement prepareCall(String sql, int resultSetType, int 
resultSetConcurrency,
+                                         int resultSetHoldability) throws 
SQLException {
+        try {
+            return connection.prepareCall(sql, resultSetType, 
resultSetConcurrency, resultSetHoldability);
+        } catch (SQLException e) {
+
+            recover(e);
+            return connection.prepareCall(sql, resultSetType, 
resultSetConcurrency, resultSetHoldability);
+        }
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql, int 
autoGeneratedKeys) throws SQLException {
+
+        try {
+            return connection.prepareStatement(sql, autoGeneratedKeys);
+        } catch (SQLException e) {
+
+            recover(e);
+            return connection.prepareStatement(sql, autoGeneratedKeys);
+        }
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql, int[] columnIndexes) 
throws SQLException {
+        try {
+            return connection.prepareStatement(sql, columnIndexes);
+        } catch (SQLException e) {
+
+            recover(e);
+            return connection.prepareStatement(sql, columnIndexes);
+        }
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql, String[] 
columnNames) throws SQLException {
+        try {
+            return connection.prepareStatement(sql, columnNames);
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.prepareStatement(sql, columnNames);
+        }
+    }
+
+    @Override
+    public Array createArrayOf(String typeName, Object[] elements) throws 
SQLException {
+        try {
+            return connection.createArrayOf(typeName, elements);
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.createArrayOf(typeName, elements);
+        }
+    }
+
+    @Override
+    public Blob createBlob() throws SQLException {
+        try {
+            return connection.createBlob();
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.createBlob();
+        }
+    }
+
+    @Override
+    public Clob createClob() throws SQLException {
+        try {
+            return connection.createClob();
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.createClob();
+        }
+    }
+
+    @Override
+    public Struct createStruct(String typeName, Object[] attributes) throws 
SQLException {
+        try {
+            return connection.createStruct(typeName, attributes);
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.createStruct(typeName, attributes);
+        }
+    }
+
+    @Override
+    public Properties getClientInfo() throws SQLException {
+        try {
+            return connection.getClientInfo();
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.getClientInfo();
+        }
+    }
+
+    @Override
+    public String getClientInfo(String name) throws SQLException {
+        try {
+            return connection.getClientInfo(name);
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.getClientInfo(name);
+        }
+    }
+
+    @Override
+    public boolean isValid(int timeout) throws SQLException {
+        try {
+            return connection.isValid(timeout);
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.isValid(timeout);
+        }
+    }
+
+    @Override
+    public boolean isWrapperFor(Class<?> iface) throws SQLException {
+        return PoolAwareConnection.class.equals(iface) || 
connection.isWrapperFor(iface);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T unwrap(Class<T> iface) throws SQLException {
+        return PoolAwareConnection.class.equals(iface) ? (T) this : 
connection.unwrap(iface);
+    }
+
+    @Override
+    public NClob createNClob() throws SQLException {
+        try {
+            return connection.createNClob();
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.createNClob();
+        }
+    }
+
+    @Override
+    public SQLXML createSQLXML() throws SQLException {
+        try {
+            return connection.createSQLXML();
+        } catch (SQLException sqlEx) {
+
+            recover(sqlEx);
+            return connection.createSQLXML();
+        }
+    }
+
+    @Override
+    public void setClientInfo(Properties properties) throws 
SQLClientInfoException {
+        connection.setClientInfo(properties);
+    }
+
+    @Override
+    public void setClientInfo(String name, String value) throws 
SQLClientInfoException {
+        connection.setClientInfo(name, value);
+    }
+
+    @Override
+    public void setSchema(String schema) throws SQLException {
+        connection.setSchema(schema);
+    }
+
+    @Override
+    public String getSchema() throws SQLException {
+        return connection.getSchema();
+    }
+
+    @Override
+    public void abort(Executor executor) throws SQLException {
+        connection.abort(executor);
+    }
+
+    @Override
+    public void setNetworkTimeout(Executor executor, int milliseconds) throws 
SQLException {
+        connection.setNetworkTimeout(executor, milliseconds);
+    }
+
+    @Override
+    public int getNetworkTimeout() throws SQLException {
+        return connection.getNetworkTimeout();
+    }
 
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSource.java 
b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSource.java
index d983cc39d..d4e770595 100644
--- a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSource.java
+++ b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSource.java
@@ -21,8 +21,7 @@ package org.apache.cayenne.datasource;
 import javax.sql.DataSource;
 
 /**
- * A {@link DataSource} that pools connections and requires to be explicitly
- * closed.
+ * A {@link DataSource} that pools connections and requires to be explicitly 
closed.
  * 
  * @since 4.0
  */
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSourceBuilder.java
 
b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSourceBuilder.java
index bd6257a3b..fd8fdd9bf 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSourceBuilder.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSourceBuilder.java
@@ -18,17 +18,19 @@
  ****************************************************************/
 package org.apache.cayenne.datasource;
 
-import javax.sql.DataSource;
-
 import org.apache.cayenne.CayenneRuntimeException;
 
+import javax.sql.DataSource;
+
 /**
  * Turns unpooled DataSource to a connection pool. Normally you won't be
  * creating this builder explicitly. Call
  * {@link DataSourceBuilder#pool(int, int)} method instead.
- * 
+ *
  * @since 4.0
+ * @deprecated in favor of {@link CayenneDataSource}
  */
+@Deprecated(since = "5.0", forRemoval = true)
 public class PoolingDataSourceBuilder {
 
        private DataSourceBuilder nonPoolingBuilder;
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntimeBuilder.java 
b/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntimeBuilder.java
index 3e8618dc4..0a5405dc0 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntimeBuilder.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntimeBuilder.java
@@ -23,7 +23,7 @@ import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.runtime.CoreModule;
 import org.apache.cayenne.configuration.runtime.DataSourceFactory;
 import org.apache.cayenne.configuration.runtime.CoreModuleExtender;
-import org.apache.cayenne.datasource.DataSourceBuilder;
+import org.apache.cayenne.datasource.CayenneDataSource;
 import org.apache.cayenne.di.Module;
 import org.apache.cayenne.di.spi.ModuleLoader;
 
@@ -88,7 +88,7 @@ public class CayenneRuntimeBuilder {
      * mapping. If the mapping contains no DataNodes, and the DataSource is set
      * with this method, the builder would create a single default DataNode.
      *
-     * @see DataSourceBuilder
+     * @see CayenneDataSource
      */
     public CayenneRuntimeBuilder dataSource(DataSource dataSource) {
         this.dataSourceFactory = new FixedDataSourceFactory(dataSource);
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/datasource/CayenneDataSourceTest.java
 
b/cayenne/src/test/java/org/apache/cayenne/datasource/CayenneDataSourceTest.java
new file mode 100644
index 000000000..9223c17c4
--- /dev/null
+++ 
b/cayenne/src/test/java/org/apache/cayenne/datasource/CayenneDataSourceTest.java
@@ -0,0 +1,196 @@
+/*****************************************************************
+ *   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.apache.cayenne.datasource;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.junit.jupiter.api.Test;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CayenneDataSourceTest {
+
+    private static final String DRIVER = "org.hsqldb.jdbcDriver";
+
+    @Test
+    public void nonPoolingWhenNoPoolSettings() throws Exception {
+        DataSource dataSource = 
CayenneDataSource.of("jdbc:hsqldb:mem:cds_non_pooling")
+                .driverClass(DRIVER)
+                .userName("sa")
+                .password("")
+                .build();
+
+        assertInstanceOf(DriverDataSource.class, dataSource);
+
+        try (Connection c = dataSource.getConnection()) {
+            assertFalse(c.isClosed());
+        }
+    }
+
+    @Test
+    public void poolingWhenPoolRequested() throws Exception {
+        DataSource dataSource = 
CayenneDataSource.of("jdbc:hsqldb:mem:cds_pooling")
+                .driverClass(DRIVER)
+                .userName("sa")
+                .password("")
+                .pool(1, 2)
+                .build();
+
+        try (PoolingDataSource pooling = 
assertInstanceOf(ManagedPoolingDataSource.class, dataSource);
+             Connection c = pooling.getConnection()) {
+            assertFalse(c.isClosed());
+        }
+    }
+
+    @Test
+    public void minGreaterThanMaxFails() {
+        CayenneDataSource.Builder builder = 
CayenneDataSource.of("jdbc:hsqldb:mem:cds_invalid")
+                .driverClass(DRIVER);
+
+        assertThrows(CayenneRuntimeException.class, () -> builder.pool(5, 2));
+    }
+
+    @Test
+    public void negativeConnectionsFail() {
+        CayenneDataSource.Builder builder = 
CayenneDataSource.of("jdbc:hsqldb:mem:cds_negative")
+                .driverClass(DRIVER);
+
+        assertThrows(CayenneRuntimeException.class, () -> builder.pool(-1, 2));
+    }
+
+    @Test
+    public void poolSettingsWithoutPoolIgnored() {
+        DataSource dataSource = 
CayenneDataSource.of("jdbc:hsqldb:mem:cds_no_pool")
+                .driverClass(DRIVER)
+                .validationQuery("SELECT 1")
+                .build();
+
+        assertInstanceOf(DriverDataSource.class, dataSource);
+    }
+
+    @Test
+    public void driverResolvedFromUrl() throws Exception {
+        DataSource dataSource = 
CayenneDataSource.of("jdbc:hsqldb:mem:cds_url_resolved")
+                .userName("sa")
+                .password("")
+                .build();
+
+        assertInstanceOf(DriverDataSource.class, dataSource);
+
+        try (Connection c = dataSource.getConnection()) {
+            assertFalse(c.isClosed());
+        }
+    }
+
+    @Test
+    public void unknownUrlWithoutDriverFails() {
+        CayenneDataSource.Builder builder = 
CayenneDataSource.of("jdbc:nosuchdb:mem:cds");
+
+        assertThrows(CayenneRuntimeException.class, builder::build);
+    }
+
+    @Test
+    public void fromPropertiesNonPooling() throws Exception {
+        Map<String, String> properties = Map.of(
+                "cayenne.jdbc.url", "jdbc:hsqldb:mem:cds_props",
+                "cayenne.jdbc.username", "sa",
+                "cayenne.jdbc.password", "");
+
+        DataSource dataSource = 
CayenneDataSource.fromProperties(properties).build();
+
+        assertInstanceOf(DriverDataSource.class, dataSource);
+
+        try (Connection c = dataSource.getConnection()) {
+            assertFalse(c.isClosed());
+        }
+    }
+
+    @Test
+    public void fromPropertiesPooling() throws Exception {
+        Map<String, String> properties = Map.of(
+                "cayenne.jdbc.url", "jdbc:hsqldb:mem:cds_props_pool",
+                "cayenne.jdbc.username", "sa",
+                "cayenne.jdbc.password", "",
+                "cayenne.jdbc.min_connections", "1",
+                "cayenne.jdbc.max_connections", "2");
+
+        DataSource dataSource = 
CayenneDataSource.fromProperties(properties).build();
+
+        try (PoolingDataSource pooling = 
assertInstanceOf(ManagedPoolingDataSource.class, dataSource);
+             Connection c = pooling.getConnection()) {
+            assertFalse(c.isClosed());
+        }
+    }
+
+    @Test
+    public void fromPropertiesWithNodeSuffixAndFallback() throws Exception {
+        Map<String, String> properties = Map.of(
+                "cayenne.jdbc.url", "jdbc:hsqldb:mem:cds_props_base",
+                "cayenne.jdbc.url.node1", "jdbc:hsqldb:mem:cds_props_node1",
+                "cayenne.jdbc.username", "sa",
+                "cayenne.jdbc.password", "");
+
+        DataSource dataSource = CayenneDataSource.fromProperties(properties, 
"node1").build();
+
+        assertInstanceOf(DriverDataSource.class, dataSource);
+
+        try (Connection c = dataSource.getConnection()) {
+            assertEquals("jdbc:hsqldb:mem:cds_props_node1", 
c.getMetaData().getURL());
+        }
+    }
+
+    @Test
+    public void fromPropertiesMissingUrlFails() {
+        Map<String, String> properties = Map.of();
+
+        assertThrows(CayenneRuntimeException.class, () -> 
CayenneDataSource.fromProperties(properties));
+    }
+
+    @Test
+    public void fromPropertiesInvalidConnectionCountTreatedAsUnset() {
+        Map<String, String> properties = Map.of(
+                "cayenne.jdbc.url", "jdbc:hsqldb:mem:cds_props_bad_int",
+                "cayenne.jdbc.username", "sa",
+                "cayenne.jdbc.password", "",
+                "cayenne.jdbc.min_connections", "not_a_number");
+
+        DataSource dataSource = 
CayenneDataSource.fromProperties(properties).build();
+
+        assertInstanceOf(DriverDataSource.class, dataSource);
+    }
+
+    @Test
+    public void fromPropertiesNullNodeNameFails() {
+        Map<String, String> properties = Map.of(
+                "cayenne.jdbc.url", "jdbc:hsqldb:mem:cds_props");
+
+        assertThrows(NullPointerException.class, () -> 
CayenneDataSource.fromProperties(properties, null));
+    }
+
+    @Test
+    public void unknownDriverFails() {
+        CayenneDataSource.Builder builder = 
CayenneDataSource.of("jdbc:example:none")
+                .driverClass("com.example.NoSuchDriver");
+
+        assertThrows(CayenneRuntimeException.class, builder::build);
+    }
+}
diff --git a/cayenne/src/test/java/org/apache/cayenne/unit/CayenneTestsEnv.java 
b/cayenne/src/test/java/org/apache/cayenne/unit/CayenneTestsEnv.java
index 769cc58b0..0b71a3184 100644
--- a/cayenne/src/test/java/org/apache/cayenne/unit/CayenneTestsEnv.java
+++ b/cayenne/src/test/java/org/apache/cayenne/unit/CayenneTestsEnv.java
@@ -25,7 +25,7 @@ import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.DataSourceDescriptor;
 import org.apache.cayenne.configuration.runtime.CoreModule;
 import org.apache.cayenne.configuration.runtime.DataNodeFactory;
-import org.apache.cayenne.datasource.DataSourceBuilder;
+import org.apache.cayenne.datasource.CayenneDataSource;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.di.AdhocObjectFactory;
@@ -64,9 +64,9 @@ public class CayenneTestsEnv implements BeforeEachCallback, 
AfterEachCallback {
 
     static {
         DataSourceDescriptor dsDescriptor = DataSourceConfigLoader.load();
-        DataSource ds = DataSourceBuilder
-                .url(dsDescriptor.getDataSourceUrl())
-                .driver(dsDescriptor.getJdbcDriver())
+        DataSource ds = CayenneDataSource
+                .of(dsDescriptor.getDataSourceUrl())
+                .driverClass(dsDescriptor.getJdbcDriver())
                 .userName(dsDescriptor.getUserName())
                 .password(dsDescriptor.getPassword())
                 .pool(dsDescriptor.getMinConnections(), 
dsDescriptor.getMaxConnections())
diff --git 
a/docs/asciidoc/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc
 
b/docs/asciidoc/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc
index beec80606..ad1ef0e1b 100644
--- 
a/docs/asciidoc/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc
+++ 
b/docs/asciidoc/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc
@@ -45,9 +45,8 @@ public class Main {
 
     public static void main(String[] args) {
         CayenneRuntime cayenneRuntime = CayenneRuntime.builder()
-            .dataSource(DataSourceBuilder
-                    .url("jdbc:mysql://127.0.0.1:3306/cayenne_demo")
-                    .driver("com.mysql.cj.jdbc.Driver")
+            .dataSource(CayenneDataSource
+                    .of("jdbc:mysql://127.0.0.1:3306/cayenne_demo")
                     .userName("root") // TODO: change to your actual username 
and password
                     .password("your-password").build())
             .addConfig("cayenne-project.xml")

Reply via email to