You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/06/29 14:06:14 UTC

(hop) branch main updated: Add downloader for JDBC drivers, fixes #7352 (#7353)

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

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 0822e16527 Add downloader for JDBC drivers, fixes #7352 (#7353)
0822e16527 is described below

commit 0822e16527e2b9638f960096e7d74e246e4dcdd8
Author: Hans Van Akelyen <ha...@gmail.com>
AuthorDate: Mon Jun 29 16:06:08 2026 +0200

    Add downloader for JDBC drivers, fixes #7352 (#7353)
---
 .../apache/hop/core/database/DriverDownload.java   |  97 +++++++
 .../org/apache/hop/core/database/IDatabase.java    |  12 +
 .../apache/hop/core/plugins/HopURLClassLoader.java |  10 +
 .../hop/core/database/DriverDownloadTest.java      | 112 +++++++++
 .../hop/core/plugins/HopURLClassLoaderTest.java    |  68 +++++
 .../integration-tests-database.yaml                |   1 +
 ...-vertica.yaml => integration-tests-duckdb.yaml} |  16 +-
 .../integration-tests-vertica.yaml                 |   4 +-
 docker/integration-tests/unit-tests.Dockerfile     |   5 -
 docker/resources/load-and-execute.sh               |  49 ++++
 docker/resources/run-web.sh                        |  48 ++++
 docker/unified.Dockerfile                          |   6 +
 .../modules/ROOT/pages/docker-container.adoc       |  54 ++++
 .../modules/ROOT/pages/database/databases.adoc     |  36 ++-
 .../ROOT/pages/database/databases/access.adoc      |   3 +-
 .../ROOT/pages/database/databases/apache-hive.adoc |   3 +-
 .../ROOT/pages/database/databases/as400.adoc       |   3 +-
 .../ROOT/pages/database/databases/cache.adoc       |   4 +-
 .../ROOT/pages/database/databases/clickhouse.adoc  |   3 +-
 .../pages/database/databases/cloudera-impala.adoc  |   2 -
 .../ROOT/pages/database/databases/cratedb.adoc     |   3 +-
 .../ROOT/pages/database/databases/databricks.adoc  |   2 -
 .../modules/ROOT/pages/database/databases/db2.adoc |   3 +-
 .../ROOT/pages/database/databases/derby.adoc       |   5 +-
 .../ROOT/pages/database/databases/doris.adoc       |   3 +-
 .../ROOT/pages/database/databases/duckdb.adoc      |   5 +-
 .../ROOT/pages/database/databases/exasol.adoc      |   3 +-
 .../ROOT/pages/database/databases/firebird.adoc    |   3 +-
 .../pages/database/databases/googlebigquery.adoc   |   2 -
 .../ROOT/pages/database/databases/greenplum.adoc   |   3 +-
 .../modules/ROOT/pages/database/databases/h2.adoc  |   3 +-
 .../ROOT/pages/database/databases/hypersonic.adoc  |   3 +-
 .../ROOT/pages/database/databases/infobright.adoc  |   3 +-
 .../ROOT/pages/database/databases/informix.adoc    |   3 +-
 .../ROOT/pages/database/databases/ingres.adoc      |   3 +-
 .../ROOT/pages/database/databases/interbase.adoc   |   2 -
 .../ROOT/pages/database/databases/iris.adoc        |   2 -
 .../ROOT/pages/database/databases/kingbasees.adoc  |   2 -
 .../ROOT/pages/database/databases/mariadb.adoc     |   3 +-
 .../ROOT/pages/database/databases/monetdb.adoc     |   3 +-
 .../ROOT/pages/database/databases/mssql.adoc       |   3 +-
 .../ROOT/pages/database/databases/mssqlnative.adoc |   3 +-
 .../ROOT/pages/database/databases/mysql.adoc       |   3 +-
 .../ROOT/pages/database/databases/netezza.adoc     |   2 -
 .../ROOT/pages/database/databases/oracle.adoc      |   5 +-
 .../ROOT/pages/database/databases/oraclerdb.adoc   |   2 -
 .../ROOT/pages/database/databases/postgresql.adoc  |   3 +-
 .../ROOT/pages/database/databases/redshift.adoc    |   3 +-
 .../ROOT/pages/database/databases/sapdb.adoc       |   2 -
 .../ROOT/pages/database/databases/singlestore.adoc |   3 +-
 .../ROOT/pages/database/databases/snowflake.adoc   |   5 +-
 .../ROOT/pages/database/databases/sqlbase.adoc     |   2 -
 .../ROOT/pages/database/databases/sqlite.adoc      |   3 +-
 .../ROOT/pages/database/databases/sybase.adoc      |   3 +-
 .../ROOT/pages/database/databases/sybaseiq.adoc    |   2 -
 .../ROOT/pages/database/databases/teradata.adoc    |   2 +-
 .../ROOT/pages/database/databases/universe.adoc    |   2 -
 .../ROOT/pages/database/databases/vectorwise.adoc  |   3 +-
 .../ROOT/pages/database/databases/vertica.adoc     |   3 +-
 engine/pom.xml                                     |  19 ++
 .../java/org/apache/hop/driver/DriverCatalog.java  | 119 +++++++++
 .../org/apache/hop/driver/DriverDefinition.java    |  70 ++++++
 .../apache/hop/driver/DriverInstallCommand.java    | 184 ++++++++++++++
 .../org/apache/hop/driver/DriverInstaller.java     | 245 ++++++++++++++++++
 .../org/apache/hop/driver/DriverListCommand.java   |  82 ++++++
 .../java/org/apache/hop/driver/DriverResolver.java | 196 +++++++++++++++
 .../main/java/org/apache/hop/driver/HopDriver.java |  63 +++++
 .../apache/hop/driver/DriverDefinitionTest.java    |  94 +++++++
 .../org/apache/hop/driver/DriverInstallerTest.java | 123 +++++++++
 .../org/apache/hop/driver/DriverResolverTest.java  |  78 ++++++
 integration-tests/scripts/run-tests.sh             |  25 ++
 lib/pom.xml                                        |  42 ++++
 .../hop/databases/access/AccessDatabaseMeta.java   |  16 ++
 .../hop/databases/as400/AS400DatabaseMeta.java     |  14 ++
 .../clickhouse/ClickhouseDatabaseMeta.java         |  16 ++
 .../hop/databases/cratedb/CrateDBDatabaseMeta.java |  16 ++
 .../apache/hop/databases/db2/DB2DatabaseMeta.java  |  14 ++
 .../hop/databases/derby/DerbyDatabaseMeta.java     |  14 ++
 plugins/databases/duckdb/pom.xml                   |  25 --
 .../hop/databases/duckdb/DuckDBDatabaseMeta.java   |  14 ++
 .../hop/databases/exasol4/Exasol4DatabaseMeta.java |  14 ++
 .../databases/firebird/FirebirdDatabaseMeta.java   |  16 ++
 .../apache/hop/databases/h2/H2DatabaseMeta.java    |  14 ++
 .../hop/databases/hive/HiveDatabaseMeta.java       |  15 ++
 .../hypersonic/HypersonicDatabaseMeta.java         |  14 ++
 .../databases/informix/InformixDatabaseMeta.java   |  14 ++
 .../hop/databases/ingres/IngresDatabaseMeta.java   |  14 ++
 .../hop/databases/mariadb/MariaDBDatabaseMeta.java |  15 ++
 .../hop/databases/monetdb/MonetDBDatabaseMeta.java |  15 ++
 .../databases/mssql/MsSqlServerDatabaseMeta.java   |  14 ++
 .../mssqlnative/MsSqlServerNativeDatabaseMeta.java |  15 ++
 .../hop/databases/mysql/MySqlDatabaseMeta.java     |  24 +-
 .../hop/databases/oracle/OracleDatabaseMeta.java   |  14 ++
 .../postgresql/PostgreSqlDatabaseMeta.java         |  20 ++
 .../databases/redshift/RedshiftDatabaseMeta.java   |  14 ++
 .../singlestore/SingleStoreDatabaseMeta.java       |  14 ++
 plugins/databases/snowflake/pom.xml                |  25 --
 .../databases/snowflake/SnowflakeDatabaseMeta.java |  14 ++
 .../hop/databases/sqlite/SqliteDatabaseMeta.java   |  14 ++
 .../hop/databases/sybase/SybaseDatabaseMeta.java   |  14 ++
 .../databases/teradata/TeradataDatabaseMeta.java   |  14 ++
 .../hop/databases/vertica/VerticaDatabaseMeta.java |  15 ++
 pom.xml                                            |   1 +
 .../database/JdbcDriverDownloadDialogTest.java     | 136 ++++++++++
 .../hop/ui/core/database/DatabaseMetaEditor.java   |  64 ++++-
 .../ui/core/database/JdbcDriverDownloadDialog.java | 278 +++++++++++++++++++++
 .../database/messages/messages_en_US.properties    |  19 ++
 107 files changed, 2783 insertions(+), 175 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/database/DriverDownload.java b/core/src/main/java/org/apache/hop/core/database/DriverDownload.java
new file mode 100644
index 0000000000..9792b37897
--- /dev/null
+++ b/core/src/main/java/org/apache/hop/core/database/DriverDownload.java
@@ -0,0 +1,97 @@
+/*
+ * 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
+ *
+ *       http://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.hop.core.database;
+
+import java.util.List;
+import lombok.Builder;
+import lombok.Getter;
+
+/**
+ * Describes how a database's JDBC driver can be downloaded on demand. A database plugin declares
+ * this from {@link IDatabase#getDriverDownload()} so the driver lives in one place - next to the
+ * rest of the database metadata - and external plugins can point Hop at their own drivers.
+ *
+ * <p>This is metadata only; it carries no driver bytes, so it is safe to ship even for restricted
+ * (Category X) drivers that Apache Hop may not bundle. Hop downloads the actual jar from Maven
+ * Central at the user's explicit request, after the user accepts the vendor license.
+ */
+@Getter
+@Builder
+public class DriverDownload {
+
+  /** Maven coordinate without version: {@code groupId:artifactId}. */
+  private final String mavenCoordinate;
+
+  /** Version resolved when the caller does not request a specific one. */
+  private final String defaultVersion;
+
+  /**
+   * ASF license category: {@code A}/{@code B} may be bundled, {@code X} must never be bundled and
+   * is only downloaded at the user's explicit request after license acceptance.
+   */
+  private final String licenseCategory;
+
+  private final String licenseName;
+  private final String licenseUrl;
+  private final String vendor;
+  private final String vendorUrl;
+  private final String notes;
+
+  /**
+   * Maven coordinates ({@code groupId:artifactId}, artifactId may be {@code *}) to exclude from the
+   * download. Use this to drop transitive jars that Hop already ships in lib/core (so the driver
+   * uses the shared one via its parent classloader), e.g. {@code
+   * com.google.protobuf:protobuf-java}. Optional and test dependencies are skipped automatically
+   * and do not need to be listed here.
+   */
+  @Builder.Default private final List<String> excludes = List.of();
+
+  /**
+   * Optional extra Maven repository base URL to resolve this driver from, for drivers that are not
+   * on Maven Central (e.g. MonetDB on Clojars). It is searched in addition to Maven Central / the
+   * user-supplied repository. Null means Maven Central only.
+   */
+  private final String repositoryUrl;
+
+  /**
+   * @return true when this is a restricted (Category X) driver that must not be bundled.
+   */
+  public boolean isRestricted() {
+    return "X".equalsIgnoreCase(licenseCategory);
+  }
+
+  /**
+   * @return the full Maven coordinate {@code groupId:artifactId:version}, using {@code version}
+   *     when supplied, otherwise {@link #defaultVersion}.
+   */
+  public String toCoordinate(String version) {
+    String v = (version == null || version.isBlank()) ? defaultVersion : version;
+    return mavenCoordinate + ":" + v;
+  }
+
+  /**
+   * @return the artifact id portion of {@link #mavenCoordinate}, used for install detection.
+   */
+  public String getArtifactId() {
+    if (mavenCoordinate == null) {
+      return null;
+    }
+    String[] parts = mavenCoordinate.split(":");
+    return parts.length >= 2 ? parts[1] : null;
+  }
+}
diff --git a/core/src/main/java/org/apache/hop/core/database/IDatabase.java b/core/src/main/java/org/apache/hop/core/database/IDatabase.java
index db9e804a84..d823890de1 100644
--- a/core/src/main/java/org/apache/hop/core/database/IDatabase.java
+++ b/core/src/main/java/org/apache/hop/core/database/IDatabase.java
@@ -263,6 +263,18 @@ public interface IDatabase extends Cloneable {
    */
   String getDriverClass();
 
+  /**
+   * Optional descriptor that lets Hop download this database's JDBC driver on demand. The default
+   * is {@code null}, meaning there is no downloadable driver. Database plugins - including
+   * external, third-party ones - override this to point Hop at their driver's Maven coordinate and
+   * license, so the driver definition lives next to the rest of the database metadata.
+   *
+   * @return the driver download descriptor, or {@code null} when none is available
+   */
+  default DriverDownload getDriverDownload() {
+    return null;
+  }
+
   /**
    * @param hostname the hostname
    * @param port the port as a string
diff --git a/core/src/main/java/org/apache/hop/core/plugins/HopURLClassLoader.java b/core/src/main/java/org/apache/hop/core/plugins/HopURLClassLoader.java
index 999661c862..91c7e25cdd 100644
--- a/core/src/main/java/org/apache/hop/core/plugins/HopURLClassLoader.java
+++ b/core/src/main/java/org/apache/hop/core/plugins/HopURLClassLoader.java
@@ -46,6 +46,16 @@ public class HopURLClassLoader extends URLClassLoader {
     return key.cast(cache.computeIfAbsent(key, k -> provider));
   }
 
+  /**
+   * Public entry point to add a jar to this classloader at runtime, e.g. a JDBC driver downloaded
+   * while Hop is running. After this the classes in the jar become loadable without a restart.
+   *
+   * @param url the jar URL to add
+   */
+  public void addJar(URL url) {
+    addURL(url);
+  }
+
   @Override
   protected void addURL(URL url) {
     super.addURL(url);
diff --git a/core/src/test/java/org/apache/hop/core/database/DriverDownloadTest.java b/core/src/test/java/org/apache/hop/core/database/DriverDownloadTest.java
new file mode 100644
index 0000000000..c84e1ae231
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/database/DriverDownloadTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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
+ *
+ *       http://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.hop.core.database;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class DriverDownloadTest {
+
+  @Test
+  void coordinateUsesDefaultVersionAndHonoursOverride() {
+    DriverDownload download =
+        DriverDownload.builder()
+            .mavenCoordinate("com.oracle.database.jdbc:ojdbc11")
+            .defaultVersion("23.5.0.24.07")
+            .licenseCategory("X")
+            .build();
+
+    assertEquals("com.oracle.database.jdbc:ojdbc11:23.5.0.24.07", download.toCoordinate(null));
+    assertEquals("com.oracle.database.jdbc:ojdbc11:21.0.0", download.toCoordinate("21.0.0"));
+    assertEquals("ojdbc11", download.getArtifactId());
+  }
+
+  @Test
+  void coordinateKeepsAnExplicitClassifier() {
+    // Some drivers ship a self-contained classified artifact, e.g. ClickHouse's "all" jar. The
+    // version is appended after the group:artifact:packaging:classifier, and the artifactId is
+    // still the second segment.
+    DriverDownload download =
+        DriverDownload.builder()
+            .mavenCoordinate("com.clickhouse:clickhouse-jdbc:jar:all")
+            .defaultVersion("0.9.8")
+            .licenseCategory("A")
+            .build();
+
+    assertEquals("com.clickhouse:clickhouse-jdbc:jar:all:0.9.8", download.toCoordinate(null));
+    assertEquals("clickhouse-jdbc", download.getArtifactId());
+  }
+
+  @Test
+  void coordinateUsesDefaultVersionWhenOverrideIsBlank() {
+    DriverDownload download =
+        DriverDownload.builder()
+            .mavenCoordinate("org.postgresql:postgresql")
+            .defaultVersion("42.7.4")
+            .build();
+
+    assertEquals("org.postgresql:postgresql:42.7.4", download.toCoordinate("   "));
+  }
+
+  @Test
+  void artifactIdIsNullForAnIncompleteCoordinate() {
+    assertNull(DriverDownload.builder().build().getArtifactId());
+    assertNull(DriverDownload.builder().mavenCoordinate("single-segment").build().getArtifactId());
+  }
+
+  @Test
+  void restrictedIsBasedOnCategoryX() {
+    assertTrue(DriverDownload.builder().licenseCategory("X").build().isRestricted());
+    assertTrue(DriverDownload.builder().licenseCategory("x").build().isRestricted());
+    assertFalse(DriverDownload.builder().licenseCategory("A").build().isRestricted());
+    assertFalse(DriverDownload.builder().build().isRestricted());
+  }
+
+  @Test
+  void databaseWithoutDownloadReturnsNullByDefault() {
+    IDatabase database = new NoneDatabaseMeta();
+    assertNull(database.getDriverDownload());
+  }
+
+  @Test
+  void excludesDefaultToEmptyAndCarryValues() {
+    assertTrue(DriverDownload.builder().build().getExcludes().isEmpty());
+    assertEquals(
+        java.util.List.of("com.google.protobuf:protobuf-java"),
+        DriverDownload.builder()
+            .excludes(java.util.List.of("com.google.protobuf:protobuf-java"))
+            .build()
+            .getExcludes());
+  }
+
+  @Test
+  void repositoryUrlDefaultsToNullAndCarriesValue() {
+    org.junit.jupiter.api.Assertions.assertNull(
+        DriverDownload.builder().build().getRepositoryUrl());
+    assertEquals(
+        "https://clojars.org/repo/",
+        DriverDownload.builder()
+            .repositoryUrl("https://clojars.org/repo/")
+            .build()
+            .getRepositoryUrl());
+  }
+}
diff --git a/core/src/test/java/org/apache/hop/core/plugins/HopURLClassLoaderTest.java b/core/src/test/java/org/apache/hop/core/plugins/HopURLClassLoaderTest.java
new file mode 100644
index 0000000000..56d4c3f344
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/plugins/HopURLClassLoaderTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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
+ *
+ *       http://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.hop.core.plugins;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Unit test for {@link HopURLClassLoader#addJar(URL)}, the hook the JDBC driver download uses to
+ * hot-load a freshly installed driver into a database plugin's classloader without a restart.
+ */
+class HopURLClassLoaderTest {
+
+  @Test
+  void addJarWiresTheJarIntoTheClasspath(@TempDir Path tempDir) throws Exception {
+    Path jar = tempDir.resolve("probe.jar");
+    try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(jar))) {
+      jos.putNextEntry(new JarEntry("probe/marker.txt"));
+      jos.write("hello".getBytes(StandardCharsets.UTF_8));
+      jos.closeEntry();
+    }
+    URL jarUrl = jar.toUri().toURL();
+
+    try (HopURLClassLoader loader =
+        new HopURLClassLoader(new URL[0], getClass().getClassLoader())) {
+      assertEquals(0, loader.getURLs().length, "the loader starts with no jars of its own");
+      assertNull(
+          loader.getResource("probe/marker.txt"),
+          "the jar's resource must not be reachable before it is added");
+
+      loader.addJar(jarUrl);
+
+      assertTrue(
+          Arrays.asList(loader.getURLs()).contains(jarUrl),
+          "addJar must add the jar to the loader's search path");
+      assertNotNull(
+          loader.getResource("probe/marker.txt"),
+          "the jar's resource must be reachable after addJar");
+    }
+  }
+}
diff --git a/docker/integration-tests/integration-tests-database.yaml b/docker/integration-tests/integration-tests-database.yaml
index d9a3c7bcce..5d82bbd7b8 100644
--- a/docker/integration-tests/integration-tests-database.yaml
+++ b/docker/integration-tests/integration-tests-database.yaml
@@ -35,6 +35,7 @@ services:
       - SSH_TUNNEL_PORT=22
       - SSH_TUNNEL_USER=hop
       - SSH_TUNNEL_PASSWORD=hop_ssh_password
+      - HOP_DRIVERS_DOWNLOAD=mysql
     volumes:
       - ./resource/mssql/mssql_bulkload.csv:/tmp/mssql_bulkload.csv
 
diff --git a/docker/integration-tests/integration-tests-vertica.yaml b/docker/integration-tests/integration-tests-duckdb.yaml
similarity index 74%
copy from docker/integration-tests/integration-tests-vertica.yaml
copy to docker/integration-tests/integration-tests-duckdb.yaml
index b6301cacec..cdf9d46298 100644
--- a/docker/integration-tests/integration-tests-vertica.yaml
+++ b/docker/integration-tests/integration-tests-duckdb.yaml
@@ -15,17 +15,13 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# DuckDB is an embedded database, so there is no database service to start. We only need the test
+# runner with the DuckDB JDBC driver, which is downloaded on demand by run-tests.sh (it is no longer
+# bundled with Hop).
 services:
-  integration_test_vertica:
+  integration_test_duckdb:
     extends:
       file: integration-tests-base.yaml
       service: integration_test
-    links:
-      - vertica 
-
-  vertica:
-    image: vertica/vertica-ce:latest
-    hostname: vertica 
-    ports:
-      - "5433"
-      - "5444"
+    environment:
+      - HOP_DRIVERS_DOWNLOAD=duckdb
diff --git a/docker/integration-tests/integration-tests-vertica.yaml b/docker/integration-tests/integration-tests-vertica.yaml
index b6301cacec..ac9020b5ea 100644
--- a/docker/integration-tests/integration-tests-vertica.yaml
+++ b/docker/integration-tests/integration-tests-vertica.yaml
@@ -20,8 +20,10 @@ services:
     extends:
       file: integration-tests-base.yaml
       service: integration_test
+    environment:
+      - HOP_DRIVERS_DOWNLOAD=vertica
     links:
-      - vertica 
+      - vertica
 
   vertica:
     image: vertica/vertica-ce:latest
diff --git a/docker/integration-tests/unit-tests.Dockerfile b/docker/integration-tests/unit-tests.Dockerfile
index c33853b125..83263cb9ae 100644
--- a/docker/integration-tests/unit-tests.Dockerfile
+++ b/docker/integration-tests/unit-tests.Dockerfile
@@ -86,11 +86,6 @@ RUN chown -R ${JENKINS_USER}:${JENKINS_GROUP} ${DEPLOYMENT_PATH}/hop \
   && cd ${DEPLOYMENT_PATH}/hop \
   && ./hop-conf.sh --generate-fat-jar=/tmp/hop-fatjar.jar
 
-
-# Download Additional drivers/dependencies
-ADD --chown=${JENKINS_USER}:${JENKINS_GROUP} https://repo1.maven.org/maven2/com/vertica/jdbc/vertica-jdbc/23.4.0-0/vertica-jdbc-23.4.0-0.jar /opt/hop/lib/jdbc/vertica-jdbc-23.4.0-0.jar
-ADD --chown=${JENKINS_USER}:${JENKINS_GROUP} https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/9.2.0/mysql-connector-j-9.2.0.jar /opt/hop/lib/jdbc/mysql-connector-j-9.2.0.jar
-
 # make volume available so that hop pipeline and workflow files can be provided easily
 VOLUME ["/files"]
 USER ${JENKINS_USER}
diff --git a/docker/resources/load-and-execute.sh b/docker/resources/load-and-execute.sh
index 1b93b5043e..0e2c2a294d 100755
--- a/docker/resources/load-and-execute.sh
+++ b/docker/resources/load-and-execute.sh
@@ -29,6 +29,52 @@ exitWithCode() {
   exit "${1}"
 }
 
+# Download JDBC drivers on container start, before Hop is launched, so they are picked up by the
+# lib/jdbc scan. Driven by environment variables:
+#   HOP_DRIVERS_DOWNLOAD        comma-separated driver ids, each optionally with a version,
+#                               e.g. "oracle,mariadb:3.4.1,mysql". Run 'hop driver list' for ids.
+#   HOP_DRIVERS_ACCEPT_LICENSE  set to "true" to accept the vendor license of restricted (Category X)
+#                               drivers (Oracle, MariaDB, MySQL, DB2, ...). Required for those.
+#   HOP_DRIVERS_MAVEN_REPO      optional Maven repository base URL (defaults to Maven Central).
+install_jdbc_drivers() {
+  local drivers="${HOP_DRIVERS_DOWNLOAD:-}"
+  if [ -z "${drivers}" ]; then
+    return 0
+  fi
+
+  local accept_flag=""
+  case "${HOP_DRIVERS_ACCEPT_LICENSE:-}" in
+  true | TRUE | True | Y | y | yes | YES | 1) accept_flag="--accept-license" ;;
+  *) ;;
+  esac
+
+  local repo_flag=""
+  if [ -n "${HOP_DRIVERS_MAVEN_REPO:-}" ]; then
+    repo_flag="--repo=${HOP_DRIVERS_MAVEN_REPO}"
+  fi
+
+  log "Installing JDBC drivers: ${drivers}"
+
+  local spec id version version_flag
+  for spec in ${drivers//,/ }; do
+    spec="$(echo "${spec}" | tr -d '[:space:]')"
+    [ -z "${spec}" ] && continue
+
+    id="${spec%%:*}"
+    version=""
+    [ "${spec}" != "${id}" ] && version="${spec#*:}"
+    version_flag=""
+    [ -n "${version}" ] && version_flag="--driver-version=${version}"
+
+    log "Installing JDBC driver '${id}'${version:+ (version ${version})}"
+    # shellcheck disable=SC2086
+    if ! "${DEPLOYMENT_PATH}"/hop driver install "${id}" ${version_flag} ${repo_flag} ${accept_flag}; then
+      log "Error: failed to install JDBC driver '${id}'"
+      exitWithCode 8
+    fi
+  done
+}
+
 #   write the hop-server config to a configuration file
 #   to avoid the password of the server being shown in ps
 #
@@ -110,6 +156,9 @@ if [ -f "${HOP_CUSTOM_ENTRYPOINT_EXTENSION_SHELL_FILE_PATH}" ]; then
   source "${HOP_CUSTOM_ENTRYPOINT_EXTENSION_SHELL_FILE_PATH}"
 fi
 
+# Download requested JDBC drivers (HOP_DRIVERS_DOWNLOAD) before Hop starts.
+install_jdbc_drivers
+
 # Set empty defaults on the Hop command options.
 #
 HOP_COMMAND="${HOP_COMMAND:-}"
diff --git a/docker/resources/run-web.sh b/docker/resources/run-web.sh
index 96f01f7a31..be86ae7e61 100755
--- a/docker/resources/run-web.sh
+++ b/docker/resources/run-web.sh
@@ -29,6 +29,52 @@ exitWithCode() {
   exit "${1}"
 }
 
+# Download JDBC drivers on container start, before Tomcat is launched, so they are picked up by the
+# lib/jdbc scan. Driven by environment variables:
+#   HOP_DRIVERS_DOWNLOAD        comma-separated driver ids, each optionally with a version,
+#                               e.g. "oracle,mariadb:3.4.1,mysql". Run 'hop driver list' for ids.
+#   HOP_DRIVERS_ACCEPT_LICENSE  set to "true" to accept the vendor license of restricted (Category X)
+#                               drivers (Oracle, MariaDB, MySQL, DB2, ...). Required for those.
+#   HOP_DRIVERS_MAVEN_REPO      optional Maven repository base URL (defaults to Maven Central).
+install_jdbc_drivers() {
+  local drivers="${HOP_DRIVERS_DOWNLOAD:-}"
+  if [ -z "${drivers}" ]; then
+    return 0
+  fi
+
+  local accept_flag=""
+  case "${HOP_DRIVERS_ACCEPT_LICENSE:-}" in
+  true | TRUE | True | Y | y | yes | YES | 1) accept_flag="--accept-license" ;;
+  *) ;;
+  esac
+
+  local repo_flag=""
+  if [ -n "${HOP_DRIVERS_MAVEN_REPO:-}" ]; then
+    repo_flag="--repo=${HOP_DRIVERS_MAVEN_REPO}"
+  fi
+
+  log "Installing JDBC drivers: ${drivers}"
+
+  local spec id version version_flag
+  for spec in ${drivers//,/ }; do
+    spec="$(echo "${spec}" | tr -d '[:space:]')"
+    [ -z "${spec}" ] && continue
+
+    id="${spec%%:*}"
+    version=""
+    [ "${spec}" != "${id}" ] && version="${spec#*:}"
+    version_flag=""
+    [ -n "${version}" ] && version_flag="--driver-version=${version}"
+
+    log "Installing JDBC driver '${id}'${version:+ (version ${version})}"
+    # shellcheck disable=SC2086
+    if ! "${DEPLOYMENT_PATH}"/hop driver install "${id}" ${version_flag} ${repo_flag} ${accept_flag}; then
+      log "Error: failed to install JDBC driver '${id}'"
+      exitWithCode 8
+    fi
+  done
+}
+
 # The common execution options for short and long lived containers
 # The default log level is Basic
 #
@@ -102,6 +148,8 @@ else
   log "Not creating a project or environment in the container"
 fi
 
+# Download requested JDBC drivers (HOP_DRIVERS_DOWNLOAD) before Tomcat starts.
+install_jdbc_drivers
 
 # if we have a /config/tomcat-users.xml file, copy it to the conf folder.
 if [ -f "/config/tomcat-users.xml" ]; then
diff --git a/docker/unified.Dockerfile b/docker/unified.Dockerfile
index e6880b9f26..737b80cf6c 100644
--- a/docker/unified.Dockerfile
+++ b/docker/unified.Dockerfile
@@ -251,6 +251,9 @@ ENV HOP_LOG_LEVEL=Basic
 ENV HOP_FILE_PATH=
 ENV HOP_LOG_PATH=${DEPLOYMENT_PATH}/hop.err.log
 ENV HOP_SHARED_JDBC_FOLDERS=
+ENV HOP_DRIVERS_DOWNLOAD=
+ENV HOP_DRIVERS_ACCEPT_LICENSE=
+ENV HOP_DRIVERS_MAVEN_REPO=
 ENV HOP_PROJECT_NAME=
 ENV HOP_PROJECT_DIRECTORY=
 ENV HOP_PROJECT_FOLDER=
@@ -325,6 +328,9 @@ ENV HOP_OPTIONS="-XX:+AggressiveHeap -Dorg.eclipse.rap.rwt.resourceLocation=/tmp
 ENV HOP_PASSWORD_ENCODER_PLUGIN="Hop"
 ENV HOP_PLUGIN_BASE_FOLDERS=${CATALINA_HOME}/plugins
 ENV HOP_SHARED_JDBC_FOLDERS="${CATALINA_HOME}/jdbc-drivers"
+ENV HOP_DRIVERS_DOWNLOAD=
+ENV HOP_DRIVERS_ACCEPT_LICENSE=
+ENV HOP_DRIVERS_MAVEN_REPO=
 ENV HOP_GUI_ZOOM_FACTOR=1.0
 ENV HOP_PROJECT_FOLDER=
 ENV HOP_PROJECT_CONFIG_FILE_NAME=project-config.json
diff --git a/docs/hop-tech-manual/modules/ROOT/pages/docker-container.adoc b/docs/hop-tech-manual/modules/ROOT/pages/docker-container.adoc
index 1393be9f25..d3d36d018e 100644
--- a/docs/hop-tech-manual/modules/ROOT/pages/docker-container.adoc
+++ b/docs/hop-tech-manual/modules/ROOT/pages/docker-container.adoc
@@ -105,6 +105,22 @@ The `-XX:+AggressiveHeap` option tells the container to use all memory assigned
 |
 | Comma separated list to locations where JDBC drivers live (default is /lib/jdbc).
 
+|```HOP_DRIVERS_DOWNLOAD```
+|
+| Comma separated list of JDBC drivers to download and install on container start, before Hop is launched.
+Each entry is a driver id (run `hop driver list` to see them), optionally followed by `:version`, for example `oracle,mariadb:3.4.1,mysql`.
+See <<Downloading JDBC drivers>>.
+
+|```HOP_DRIVERS_ACCEPT_LICENSE```
+|
+| Set to `true` to accept the vendor license of restricted (Category X) drivers such as Oracle, MySQL, MariaDB and DB2.
+Required to download those drivers, since Apache Hop does not distribute them.
+
+|```HOP_DRIVERS_MAVEN_REPO```
+|
+| Optional Maven repository base URL to download the drivers from (defaults to Maven Central).
+Point this at an internal Nexus or Artifactory for air-gapped environments.
+
 |```HOP_CUSTOM_ENTRYPOINT_EXTENSION_SHELL_FILE_PATH```
 |
 | Optional path to a custom entrypoint extension script file.
@@ -329,6 +345,44 @@ In this example, a project is created in the container and exposed in the Hop Py
 
 NOTE: The option to run commands is available from Hop version 2.18.
 
+== Downloading JDBC drivers
+
+Apache Hop ships the JDBC drivers it is allowed to redistribute (PostgreSQL, MS SQL Server, Snowflake, ...). Drivers with a restricted license - Oracle, MySQL, MariaDB, IBM DB2 and others - are *not* bundled and have to be added to the `lib/jdbc` folder yourself.
+
+The container can do this for you on start-up. Set `HOP_DRIVERS_DOWNLOAD` to a comma separated list of driver ids and the entrypoint runs `hop driver install` for each one *before* Hop is launched, so the drivers are picked up by the normal `lib/jdbc` scan. Each entry is a driver id, optionally followed by `:version`. Run `hop driver list` (inside the container or a local Hop install) to see the available ids.
+
+Because the restricted drivers are downloaded from the vendor / Maven Central under the vendor's license, you must explicitly accept that license by setting `HOP_DRIVERS_ACCEPT_LICENSE=true`. Without it, restricted drivers are skipped with an error. Apache Hop neither hosts nor redistributes these drivers; obtaining them this way is your action under the vendor's terms.
+
+.Download the Oracle and MariaDB drivers in a short-lived container
+[source,shell]
+----
+docker run -it --rm \
+  --env HOP_DRIVERS_DOWNLOAD="oracle,mariadb:3.4.1" \
+  --env HOP_DRIVERS_ACCEPT_LICENSE=true \
+  --env HOP_PROJECT_FOLDER=/project \
+  --env HOP_PROJECT_NAME=my-project \
+  --env HOP_FILE_PATH='${PROJECT_HOME}/main.hpl' \
+  --env HOP_RUN_CONFIG=local \
+  --mount type=bind,source=/path/to/project,target=/project \
+  apache/hop
+----
+
+For air-gapped environments, point the download at an internal Maven mirror with `HOP_DRIVERS_MAVEN_REPO`:
+
+[source,shell]
+----
+docker run -it --rm \
+  --env HOP_DRIVERS_DOWNLOAD="oracle" \
+  --env HOP_DRIVERS_ACCEPT_LICENSE=true \
+  --env HOP_DRIVERS_MAVEN_REPO=https://nexus.internal/repository/maven-public/ \
+  ... \
+  apache/hop
+----
+
+The download is supported by the client/server image (running pipelines, workflows or `hop-server`) and the Hop web image. The drivers and their transitive dependencies are written to `HOP_SHARED_JDBC_FOLDERS` (default `lib/jdbc`). If the container filesystem is read-only, make sure that folder is writable, for example by mounting a volume over it.
+
+NOTE: `HOP_DRIVERS_DOWNLOAD` is available from Hop version 2.19.
+
 == Custom Entrypoint Extension Shell Script
 
 To make the Hop Docker image even more flexible, we added a ```HOP_CUSTOM_ENTRYPOINT_EXTENSION_SHELL_FILE_PATH``` variable that accepts a path to a custom shell script (that you provide).This shell script will run when you start the container before your Hop project is registered with the container's Hop config and before your Hop workflow or pipeline gets kicked off.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc
index be9a58a4c4..eae9f201ee 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc
@@ -26,18 +26,42 @@ Apache Hop has optimized support for many database types. If you are using an un
 To create a database connection go to the metadata perspective, right-click on `Relational Database Connections` and select `New`.
 
 The connection is saved in a central location and can then be used by all pipelines and workflows.
-If you have set your project to work with Hop, the database information will be in the `{openvar}PROJECT_HOME{closevar}/metadata/rdbms` folder. For each connection, separate a .json file will be generated in this folder. This json file will have the name of the connection and will contain all of the connection information.
+If you have set your project to work with Hop, the database information will be in the `{openvar}PROJECT_HOME{closevar}/metadata/rdbms` folder. For each connection, a separate .json file will be generated in this folder. This json file will have the name of the connection and will contain all of the connection information.
 
 == Adding JDBC drivers
 
-Apache Hop includes JDBC drivers for all databases that provide their drivers under a license that is compatible with the Apache Public License. All JDBC drivers that come with Apache Hop can be found in `lib/jdbc` in your Apache Hop installation folder.
+Apache Hop ships the JDBC drivers it is allowed to redistribute - those under a license compatible with the Apache License - in the `lib/jdbc` folder of your installation. For databases whose driver has a restricted license, or that simply isn't bundled, Hop can download the driver for you on demand, or you can add it manually.
 
-The documentation for the various database types in following subsections will show you whether the driver for your database is included. If it included, the connection editor will also show the version of the installed driver. If the driver isn't included, the documentation will guide you to the download location.
+The connection editor shows the installed driver and its version for the selected database type. When no driver is installed but one is available to download, a *Download driver* button appears next to it.
 
-Set the `HOP_SHARED_JDBC_FOLDERS` environment variable to a folder that contains your additional JDBC folders. Having them in a central folder helps you to easily upgrade or change your Apache Hop installation, without the need to add your JDBC drivers every time.
-This variable accepts a comma separated list to point to multiple directories the default value when not set is `lib/jdbc`, e.g. `<PATH_TO_YOUR_HOP_INSTALLATION>/lib/jdbc,<PATH_TO_YOUR_JDBC_FOLDER>`.
+=== Downloading a driver on demand
 
-To avoid conflicts, make sure you only have on driver for each database, and make sure you don't have multiple copies of your drivers, e.g. both in `HOP_SHARED_JDBC_FOLDERS` and `hop/lib/jdbc` folder.
+Apache Hop knows the Maven coordinates of the JDBC driver for most database types and can download it - together with the dependencies it needs - from a public repository (Maven Central by default) at your request. There are three ways to do this:
+
+* *In the GUI*: open the database connection editor and select your database type. If the driver isn't installed yet and a download is available, click *Download driver*. For restricted drivers (proprietary, or copyleft licenses such as LGPL/GPL) you first have to read and accept the vendor's license in the dialog. The driver is downloaded into your JDBC folder and hot-loaded, so you can usually test the connection without restarting Hop.
+
+* *From the command line*: `hop driver list` lists every database type with a downloadable driver, its license category and whether it is already installed. `hop driver install <id>` downloads and installs it, for example:
++
+[source,shell]
+----
+hop driver list
+hop driver install oracle --accept-license
+----
++
+`--accept-license` is required for restricted (`LIC=X`) drivers. Use `--driver-version` to pick a specific version and `--repo` to download from an internal mirror.
+
+* *In a container*: set the `HOP_DRIVERS_DOWNLOAD` environment variable (and `HOP_DRIVERS_ACCEPT_LICENSE=true` for restricted drivers) on the Hop Docker image to install drivers automatically at start-up. See the Docker container documentation for details.
+
+Apache Hop never bundles, hosts or mirrors restricted drivers. They are downloaded from Maven Central (or the vendor) onto your own machine, at your explicit request, under the vendor's license.
+
+=== Adding a driver manually
+
+Some databases (typically older or proprietary ones) do not publish their JDBC driver to a public repository. For those, download the driver from the vendor - the documentation for each database type below points you to the download location - and copy the jar into the `lib/jdbc` folder or into a folder listed in `HOP_SHARED_JDBC_FOLDERS`.
+
+Set the `HOP_SHARED_JDBC_FOLDERS` environment variable to a folder that contains your additional JDBC drivers. Having them in a central folder helps you to easily upgrade or change your Apache Hop installation, without the need to add your JDBC drivers every time.
+This variable accepts a comma separated list to point to multiple directories; the default value when not set is `lib/jdbc`, e.g. `<PATH_TO_YOUR_HOP_INSTALLATION>/lib/jdbc,<PATH_TO_YOUR_JDBC_FOLDER>`. When you download a driver, Hop installs it into the first folder in this list that isn't `lib/jdbc`, so your downloaded drivers stay separate from the bundled ones and survive upgrades.
+
+To avoid conflicts, make sure you only have one driver for each database, and make sure you don't have multiple copies of your drivers, e.g. both in `HOP_SHARED_JDBC_FOLDERS` and the `hop/lib/jdbc` folder.
 
 == Generic connection
 
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/access.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/access.adoc
index 62cc6ca3f9..e173a11166 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/access.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/access.adoc
@@ -31,9 +31,8 @@ For writing it is better to use the Microsoft Access Output transformation.
 | Option | Info
 |Type | Relational
 |Driver | Included
-|Version Included | 5.1.5
+|Install | `hop driver install msaccess`
 |Hop Dependencies | None
 |Documentation | https://spannm.github.io/ucanaccess/10-ucanaccess.html[Documentation Link]
 |JDBC Url | jdbc:ucanaccess://path.mdb
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/apache-hive.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/apache-hive.adoc
index 05d515a691..f9b4d8e648 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/apache-hive.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/apache-hive.adoc
@@ -27,11 +27,10 @@ under the License.
 | Feature | Info
 |Type | Relational
 |Driver | Included
-|Version Included | 3.1.3
+|Install | `hop driver install hive`
 |Hop Dependencies | None
 |Documentation | https://cwiki.apache.org/confluence/display/Hive/HiveServer2+Clients#HiveServer2Clients-JDBC[Documentation Link]
 |JDBC Url | jdbc:hive2://<host:port>,<host2:port2>/databaseName
-|Driver folder | Hop Installation/plugins/databases/hive/lib
 |===
 
 == Partitions
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/as400.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/as400.adoc
index ddca25c866..a859b6fc77 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/as400.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/as400.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included
-|Version Included | 21.0.0
+|Install | `hop driver install as/400`
 |Hop Dependencies | None
 |Documentation | https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_71/rzahh/javadoc/com/ibm/as400/access/doc-files/JDBCProperties.html[Documentation Link]
 |JDBC Url | jdbc:as400://hostname/default-schema
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/cache.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/cache.adoc
index 13d634c0e6..294b40bd69 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/cache.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/cache.adoc
@@ -25,11 +25,9 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=BGJD_intro
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | https://cedocs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=BGJD[Documentation Link]
 |JDBC Url  | jdbc:Cache://hostname:1972/database
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
 
-NOTE: Old driver connection, prefer new InterSystems Iris connection.
\ No newline at end of file
+NOTE: Old driver connection, prefer new InterSystems Iris connection.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/clickhouse.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/clickhouse.adoc
index 132900c265..be9439e274 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/clickhouse.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/clickhouse.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Columnar
 |Driver | Included
-|Version Included | 0.9.8
+|Install | `hop driver install clickhouse`
 |Hop Dependencies | None
 |Documentation | https://github.com/blynkkk/clickhouse4j[Documentation Link]
 |JDBC Url | jdbc:clickhouse://<host>:<port>[/<database>]
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/cloudera-impala.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/cloudera-impala.adoc
index 61ad353b79..77abfee4b4 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/cloudera-impala.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/cloudera-impala.adoc
@@ -41,9 +41,7 @@ Check the https://docs.cloudera.com/documentation/other/connectors/impala-jdbc/l
 | Option | Info
 |Type | Relational
 |Driver | Use Cloudera Impala Drivers.  You can https://www.cloudera.com/downloads/connectors/impala/jdbc/2-6-35.html[Download] the latest versions from the Cloudera website.
-|Version Included | None
 |Hop Dependencies | Cloudera Impala Database Plugin
 |Documentation | https://docs.cloudera.com/documentation/other/connectors/impala-jdbc/latest.html[Documentation Link]
 |JDBC Url | jdbc:impala://hostname:port;principal=principalName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/cratedb.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/cratedb.adoc
index 841f854fe4..f63115f54f 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/cratedb.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/cratedb.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included
-|Version Included | 2.7.0
+|Install | `hop driver install cratedb`
 |Hop Dependencies | None
 |Documentation | https://cratedb.com/docs/jdbc/en/latest/index.html[Documentation Link]
 |JDBC Url  | jdbc:crate://host:port/database
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/databricks.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/databricks.adoc
index b3bce56ff6..922b45c8c9 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/databricks.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/databricks.adoc
@@ -29,10 +29,8 @@ WARNING: the database name field is not used in a Databricks connection. Use the
 | Option | Info
 |Type | Relational
 |Driver | https://docs.databricks.com/aws/en/integrations/jdbc/download[Driver Link]
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | https://docs.databricks.com/aws/en/integrations/jdbc/[Documentation Link]
 |JDBC Url  | jdbc:databricks://<server-hostname>:443;httpPath=<http-path>[;<setting1>=<value1>;<setting2>=<value2>;<settingN>=<valueN>]
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
 
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/db2.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/db2.adoc
index 88e4e2c078..d182ea70ef 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/db2.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/db2.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://www.ibm.com/support/pages/db2-jdbc-driver-versions-and-downloads[Driver Link]
-|Version Included | None
+|Install | `hop driver install db2 --accept-license`
 |Hop Dependencies | None
 |Documentation | https://www.ibm.com/support/knowledgecenter/SSCQGF_7.2.0.1/com.ibm.IBMDI.doc_7.2.0.1/rg_conn_jdbc.html[Documentation Link]
 |JDBC Url  | jdbc:db2://hostname:port/dbname
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/derby.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/derby.adoc
index 37124bf27c..517c4c0f68 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/derby.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/derby.adoc
@@ -25,12 +25,11 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included
-|Version Included | 10.17.1.0
+|Install | `hop driver install derby`
 |Hop Dependencies | None
 |Documentation | https://db.apache.org/derby/derby_downloads.html[Documentation Link]
 |JDBC Url | jdbc:derby:<host>[:<port>]/<database>
 |JDBC Url Embedded | jdbc:derby:<database>
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
 
-NOTE: Derby JDBC driver has two different modes, if you want to use Derby Embbeded, replace the _derbyclient.jar_ file with _derby.jar_
\ No newline at end of file
+NOTE: Derby JDBC driver has two different modes, if you want to use Derby Embbeded, replace the _derbyclient.jar_ file with _derby.jar_
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/doris.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/doris.adoc
index 27137208a2..faece2cba2 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/doris.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/doris.adoc
@@ -28,11 +28,10 @@ Please see the https://doris.apache.org[Apache Doris website] for more informati
 | Option | Info
 |Type | Relational
 |Driver | https://dev.mysql.com/downloads/connector/j/[Drivere Link] (Use Platform Independent)
-|Version Included | None
+|Install | `hop driver install doris --accept-license`
 |Hop Dependencies | None
 |Documentation | https://dev.mysql.com/doc/connector-j/8.0/en/[Documentation Link]
 |JDBC Url | jdbc:mysql://hostname:9030/databaseName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
 
 The port of JDBC Url is same as the query port of Frontend in Doris.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/duckdb.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/duckdb.adoc
index 9870136e7e..609c76579d 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/duckdb.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/duckdb.adoc
@@ -35,12 +35,11 @@ For details, please refer to https://duckdb.org/docs/api/java[DuckDB Java API do
 |===
 | Option | Info
 |Type | Relational
-|Driver | https://search.maven.org/artifact/org.duckdb/duckdb_jdbc/1.3.2.1/jar[Driver Link]
-|Version Included | 1.5.3.0
+|Driver | Not bundled because of its size - install on demand
+|Install | `hop driver install duckdb`
 |Hop Dependencies | None
 |Documentation | https://duckdb.org/docs/api/java.html
 |JDBC Url | jdbc:duckdb: (in memory) or jdbc:duckdb:<FILE_PATH>
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
 
 
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/exasol.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/exasol.adoc
index 04d2a52c92..d0e066daab 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/exasol.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/exasol.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://docs.exasol.com/db/latest/connect_exasol/drivers/jdbc.htm#JDBCDriver
-|Version Included | None
+|Install | `hop driver install exasol4 --accept-license`
 |Hop Dependencies | None
 |Documentation | https://docs.exasol.com/connect_exasol/drivers/jdbc.htm[Documentation Link]
 |JDBC Url | jdbc:exa:<server>:<port8563>
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/firebird.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/firebird.adoc
index a24cb7a89a..c62780258a 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/firebird.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/firebird.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://firebirdsql.org/en/jdbc-driver/[Driver Link]
-|Version Included | None
+|Install | `hop driver install firebird --accept-license`
 |Hop Dependencies | None
 |Documentation | https://firebirdsql.github.io/jaybird-manual/jaybird_manual.html[Documentation Link]
 |JDBC Url | jdbc:firebirdsql://localhost:3050/c:/database/example.fdb
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/googlebigquery.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/googlebigquery.adoc
index 47955de64d..dde2af87ae 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/googlebigquery.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/googlebigquery.adoc
@@ -27,11 +27,9 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://cloud.google.com/bigquery/docs/reference/odbc-jdbc-drivers[Driver Link]
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | https://www.simba.com/products/BigQuery/doc/JDBC_InstallGuide/content/jdbc/d-intro.htm[Documentation Link]
 |JDBC Url | jdbc:bigquery://[Host]:[Port];ProjectId=[Project];OAuthType=[AuthValue]
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
 
 == Driver Installation
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/greenplum.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/greenplum.adoc
index 67550a512f..0825f09c50 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/greenplum.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/greenplum.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://docs.vmware.com/en/VMware-Greenplum/7/greenplum-database/datadirect-datadirect_jdbc.html
-|Version Included | None
+|Install | `hop driver install greenplum`
 |Hop Dependencies | Postgresql Plugin
 |Documentation | https://gpdb.docs.pivotal.io/590/datadirect/datadirect_jdbc.html[Documentation Link]
 |JDB Url | jdbc:pivotal:greenplum://host:port;DatabaseName=<name>
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/h2.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/h2.adoc
index 285297807f..7704a5ec74 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/h2.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/h2.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included
-|Version Included | 2.4.240
+|Install | `hop driver install h2`
 |Hop Dependencies | None
 |Documentation | https://jdbc.postgresql.org/documentation/head/index.html[Documentation Link]
 |JDBC Url | jdbc:h2:[file:][<path>]<databaseName>
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/hypersonic.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/hypersonic.adoc
index b9a6e9755e..51cd66ef88 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/hypersonic.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/hypersonic.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included
-|Version Included | 2.7.4
+|Install | `hop driver install hypersonic`
 |Hop Dependencies | None
 |Documentation | http://hsqldb.org/doc/2.0/guide/dbproperties-chapt.html[Documentation Link]
 |JDBC Url | jdbc:hsqldb:hsql//hostname
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/infobright.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/infobright.adoc
index ec6e1d4880..773f086fad 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/infobright.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/infobright.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Use MySQL Drivers
-|Version Included | None
+|Install | `hop driver install infobright --accept-license`
 |Hop Dependencies | Mysql Database Plugin
 |Documentation | https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference.html[Documentation Link]
 |JDBC Url | jdbc:mysql://hostname:3306/databaseName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/informix.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/informix.adoc
index cdc82b360e..aeadf427ad 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/informix.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/informix.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://download.oracle.com/otndocs/jcp/jdbc-4.0-fr-eval-oth-JSpec/[Driver Link]
-|Version Included | None
+|Install | `hop driver install informix --accept-license`
 |Hop Dependencies | None
 |Documentation | https://www.ibm.com/support/knowledgecenter/en/SSGU8G_12.1.0/com.ibm.jdbc_pg.doc/jdbc.htm[Documentation Link]
 |JDBC Url | jdbc:informix-sqli://hostname:1533/databaseName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/ingres.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/ingres.adoc
index 453f4f1474..6ebffcef8c 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/ingres.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/ingres.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://esd.actian.com/product/drivers/JDBC/java/JDBC[Driver Link]
-|Version Included | None
+|Install | `hop driver install ingres --accept-license`
 |Hop Dependencies | None
 |Documentation | https://docs.actian.com/ingres/11.0/index.html#page/Connectivity%2FJDBC_Driver_and_Data_Source_Classes.htm%23[Documentation Link]
 |JDBC Url | jdbc:ingres://host:port{,port}{;host:port{,port}}/db{;attr=value}
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/interbase.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/interbase.adoc
index 4411a67d56..420e9b3c08 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/interbase.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/interbase.adoc
@@ -25,9 +25,7 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included in Database installation
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | http://docwiki.embarcadero.com/InterBase/2020/en/Programming_with_JDBC[Documentation Link]
 |JDBC Url | jdbc:interbase://hostname:3050/path/to/database.ib
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/iris.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/iris.adoc
index 356e2a85e5..d5dc9250a4 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/iris.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/iris.adoc
@@ -25,9 +25,7 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://intersystems-community.github.io/iris-driver-distribution/[Driver Link]
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | https://docs.intersystems.com/iris20223/csp/docbook/Doc.View.cls?KEY=BJAVA_connecting[Documentation Link]
 |JDBC Url  | jdbc:IRIS://hostname:port/database
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/kingbasees.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/kingbasees.adoc
index ec04ab322f..d1b3b6d37c 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/kingbasees.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/kingbasees.adoc
@@ -25,9 +25,7 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver |
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | https://www.kingbase.com.cn/zhuanti/kes/html/jdbc.html[Documentation Link]
 |JDBC Url | jdbc:kingbase8://host:port/database
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/mariadb.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/mariadb.adoc
index f5c9547bce..3e05be4089 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/mariadb.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/mariadb.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://mariadb.com/kb/en/about-mariadb-connector-j/[Driver Link]
-|Version Included | None
+|Install | `hop driver install mariadb --accept-license`
 |Hop Dependencies | Mysql Database plugin
 |Documentation | https://mariadb.com/kb/en/about-mariadb-connector-j/[Documentation Link]
 |JDBC Url | jdbc:mariadb://hostname:port/databaseName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/monetdb.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/monetdb.adoc
index bb744b00bd..597bb3a79d 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/monetdb.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/monetdb.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Column Store
 |Driver | Included
-|Version Included | 12.2
+|Install | `hop driver install monetdb`
 |Hop Dependencies | None
 |Documentation | https://www.monetdb.org/Documentation/Manuals/SQLreference/Programming/JDBC[Documentation Link]
 |JDBC Url |  jdbc:monetdb://hostname/databaseName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/mssql.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/mssql.adoc
index e51cc9e506..9a3ea3cf72 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/mssql.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/mssql.adoc
@@ -28,9 +28,8 @@ When creating new database connections it is recommended to use the native conne
 | Option | Info
 |Type | Relational
 |Driver | https://sourceforge.net/projects/jtds/files/jtds/[Driver Link]
-|Version Included | None
+|Install | `hop driver install mssql --accept-license`
 |Hop Dependencies | None
 |Documentation | http://jtds.sourceforge.net/faq.html[Documentation Link]
 |JDBC Url | jdbc:jtds:sqlserver://<server>[:<port>][/<database>][;<property>=<value>[;...]]
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/mssqlnative.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/mssqlnative.adoc
index 3b1c41fa14..2bbd52e921 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/mssqlnative.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/mssqlnative.adoc
@@ -26,11 +26,10 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://docs.microsoft.com/en-us/sql/connect/jdbc/download-microsoft-jdbc-driver-for-sql-server?view=sql-server-ver15[Driver Link]
-|Version Included | 13.2.1.jre11
+|Install | `hop driver install mssqlnative`
 |Hop Dependencies | None
 |Documentation | https://docs.microsoft.com/en-us/sql/connect/jdbc/setting-the-connection-properties?view=sql-server-ver15[Documentation Link]
 |JDBC Url | jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]]
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
 
 == Integrated Authentication / Windows Based Authentication
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/mysql.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/mysql.adoc
index eb0ce1abcd..5ad1d7372d 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/mysql.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/mysql.adoc
@@ -25,11 +25,10 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://dev.mysql.com/downloads/connector/j/[Driver Link] (Use Platform Independent)
-|Version Included | None
+|Install | `hop driver install mysql --accept-license`
 |Hop Dependencies | None
 |Documentation | https://dev.mysql.com/doc/connector-j/8.0/en/[Documentation Link]
 |JDBC Url | jdbc:mysql://hostname:3306/databaseName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
 
 **Important:** When creating a connection for mysql make sure you select the correct "database type". +
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/netezza.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/netezza.adoc
index 7ea829c00e..1d8cf0da73 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/netezza.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/netezza.adoc
@@ -25,9 +25,7 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included in database installation
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | https://www.ibm.com/support/knowledgecenter/SSULQD_7.2.1/com.ibm.nz.datacon.doc/c_datacon_installing_configuring_jdbc.html[Documentation Link]
 |JDBC Url | jdbc:netezza://hostname:5490/databaseName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/oracle.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/oracle.adoc
index a9d0834104..ccf5948caa 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/oracle.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/oracle.adoc
@@ -25,11 +25,10 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://www.oracle.com/database/technologies/appdev/jdbc-downloads.html[Driver Link]
-|Version Included | None
+|Install | `hop driver install oracle --accept-license`
 |Hop Dependencies | None
 |Documentation | https://docs.oracle.com/cd/E11882_01/java.112/e16548/toc.htm[Documentation Link]
 |JDBC Url | jdbc:oracle:thin:@hostname:port Number:databaseName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
 
 TIP: Starting with Oracle Database 11g Release 1 (11.1), data type `Date` will be mapped to `Timestamp` by default.
@@ -50,4 +49,4 @@ Put the service name with `/` prefix in the database name
 Put the TNS description in the database name and leave the hostname and port empty.
 
 * If you want to use a specific TNS_ADMIN, you providing the path for tnsnames.ora through TNS_ADMIN property: `jdbc:oracle:thin:@mydb.mydomain?TNS_ADMIN=C:\\Temp\\tns` +
-Use manual connection url (not the options tab because Apache Hop does not pass options in the url for Oracle). 
\ No newline at end of file
+Use manual connection url (not the options tab because Apache Hop does not pass options in the url for Oracle). 
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/oraclerdb.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/oraclerdb.adoc
index c733199ac6..5e2d0c2a79 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/oraclerdb.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/oraclerdb.adoc
@@ -25,9 +25,7 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://www.oracle.com/database/technologies/rdb-related-products-downloads.html[Driver Link]
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | https://www.oracle.com/technetwork/database/database-technologies/rdb/documentation/rdbjdbc-ug-725-129654.pdf[Documentation Link]
 |JDBC Url | jdbc:rdbThin://<node>:<port>/<database_specification>
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/postgresql.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/postgresql.adoc
index e5a52f76e4..fe93bf3f99 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/postgresql.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/postgresql.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included
-|Version Included | 42.7.10
+|Install | `hop driver install postgresql`
 |Hop Dependencies | None
 |Documentation | https://jdbc.postgresql.org/documentation/head/index.html[Documentation Link]
 |JDBC Url  | jdbc:postgresql://host:port/database
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/redshift.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/redshift.adoc
index be4cdc1892..2a91dc5289 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/redshift.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/redshift.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included
-|Version Included | 2.2.7
+|Install | `hop driver install redshift`
 |Hop Dependencies | Postgresql Database plugin
 |Documentation | https://docs.aws.amazon.com/redshift/latest/mgmt/configure-jdbc-connection.html[Documentation Link]
 |JDBC Url | jdbc:redshift://endpoint:port/database
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/sapdb.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/sapdb.adoc
index 01972f9c29..d24e91ed28 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/sapdb.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/sapdb.adoc
@@ -25,9 +25,7 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included in Database installation
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | https://help.sap.com/saphelp_tm93/helpdata/en/37/5f6b6e966242aead8025bdc5296489/frameset.htm[Documentation Link]
 |JDBC Url | jdbc:sapdb://<database_computer>[:<port>]/<database_name>
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/singlestore.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/singlestore.adoc
index 7be38eaea8..9378e860f2 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/singlestore.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/singlestore.adoc
@@ -34,9 +34,8 @@ We suggest you place the driver JAR file either in the `lib/jdbc` folder or in t
 | Option | Info
 |Type | Relational
 |Driver | Download from the SingleStore website
-|Version Included | None
+|Install | `hop driver install singlestore --accept-license`
 |Hop Dependencies | None
 |Documentation | https://docs.singlestore.com/cloud/developer-resources/connect-with-application-development-tools/connect-with-java-jdbc/the-singlestore-jdbc-driver/[Documentation Link]
 |JDBC Url | jdbc:singlestore://<database_computer>[:<port>]/<database_name>
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/snowflake.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/snowflake.adoc
index 59122ac030..d192a7c703 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/snowflake.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/snowflake.adoc
@@ -27,12 +27,11 @@ under the License.
 |===
 | Option | Info
 |Type | Relational
-|Driver | Included
-|Version Included | 4.3.0
+|Driver | Not bundled because of its size - install on demand
+|Install | `hop driver install snowflake`
 |Hop Dependencies | None
 |Documentation | https://docs.snowflake.net/manuals/user-guide/jdbc-configure.html[Documentation Link]
 |JDBC Url | `jdbc:snowflake://<account_name>.snowflakecomputing.com/?<connection_params>`
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
 
 == Establishing a Snowflake SSL Authenticated Connection.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/sqlbase.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/sqlbase.adoc
index c3b1ccc8b5..a2858d63c5 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/sqlbase.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/sqlbase.adoc
@@ -25,9 +25,7 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://knowledge.opentext.com/knowledge/cs.dll/kcs/kb[Driver Link]
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | https://knowledge.opentext.com/knowledge/cs.dll/kcs/kb[Documentation Link]
 |JDBC Url | jdbc:sqlbase://hostname:port/databaseName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/sqlite.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/sqlite.adoc
index a3eedb8921..a04e8e88e9 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/sqlite.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/sqlite.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included
-|Version Included | 3.53.2.0
+|Install | `hop driver install sqlite`
 |Hop Dependencies | None
 |Documentation | https://www.sqlitetutorial.net/sqlite-java/sqlite-jdbc-driver/[Documentation Link]
 |JDBC Url | jdbc:sqlite:sample.db
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/sybase.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/sybase.adoc
index 055060332f..daf2f226f8 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/sybase.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/sybase.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included in Database installation
-|Version Included | None
+|Install | `hop driver install sybase --accept-license`
 |Hop Dependencies | None
 |Documentation | http://jtds.sourceforge.net/faq.html[Documentation Link]
 |JDBC Url | jdbc:jtds:sybase://<server>[:<port>][/<database>][;<property>=<value>[;...]]
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/sybaseiq.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/sybaseiq.adoc
index 40b8c1f1ae..a36a997018 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/sybaseiq.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/sybaseiq.adoc
@@ -25,9 +25,7 @@ under the License.
 | Option | Info
 |Type | Column Oriented
 |Driver | Included in database installation
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | http://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.infocenter.dc01776.1600/doc/html/san1357754910584.html[Documentation Link]
 |JDBC Url | jdbc:sybase:Tds:localhost:2638
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/teradata.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/teradata.adoc
index e1872aff60..8fe5f2b595 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/teradata.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/teradata.adoc
@@ -25,8 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://downloads.teradata.com/download/connectivity/jdbc-driver[Driver Link]
+|Install | `hop driver install teradata --accept-license`
 |Hop Dependencies | None
 |Documentation | https://teradata-docs.s3.amazonaws.com/doc/connectivity/jdbc/reference/current/frameset.html[Documentation Link]
 |JDBC Url | jdbc:teradata://Hostname
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/universe.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/universe.adoc
index 7ba8566591..d99c41dddc 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/universe.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/universe.adoc
@@ -25,9 +25,7 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | Included in database installation
-|Version Included | None
 |Hop Dependencies | None
 |Documentation | https://www-05.ibm.com/e-business/linkweb/publications/servlet/pbi.wss?CTY=US&FNC=SRX&PBL=G251-1210-00#[Documentation Link]
 |JDBC Url | jdbc:ibmu2://localhost
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/vectorwise.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/vectorwise.adoc
index 3f5f2a969b..f6c5f1c198 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/vectorwise.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/vectorwise.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://esd.actian.com/product/drivers/JDBC/java/JDBC
-|Version Included | None
+|Install | `hop driver install vectorwise --accept-license`
 |Hop Dependencies | None
 |Documentation | https://docs.actian.com/ingres/11.0/index.html#page/Connectivity%2FJDBC_Driver_and_Data_Source_Classes.htm%23[Documentation Link]
 |JDBC Url | jdbc:ingres://host:port{,port}{;host:port{,port}}/db{;attr=value}
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases/vertica.adoc b/docs/hop-user-manual/modules/ROOT/pages/database/databases/vertica.adoc
index 525a87fa7d..0db21729b3 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases/vertica.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases/vertica.adoc
@@ -25,9 +25,8 @@ under the License.
 | Option | Info
 |Type | Relational
 |Driver | https://www.vertica.com/download/vertica/client-drivers/[Driver Link]
-|Version Included | None
+|Install | `hop driver install vertica --accept-license`
 |Hop Dependencies | None
 |Documentation | https://www.vertica.com/docs/9.2.x/HTML/Content/Authoring/ConnectingToVertica/ClientJDBC/JDBCConnectionProperties.htm[Documentation Link]
 |JDBC Url | jdbc:vertica://VerticaHost:portNumber/databaseName
-|Driver folder | <Hop Installation>/lib/jdbc
 |===
diff --git a/engine/pom.xml b/engine/pom.xml
index 44f01bc7fe..0d490c0f3e 100644
--- a/engine/pom.xml
+++ b/engine/pom.xml
@@ -116,6 +116,25 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <!-- Maven Artifact Resolver (ex-Eclipse Aether): downloads JDBC drivers + their transitive
+             dependencies for the 'hop driver install' command (org.apache.hop.driver). Versions
+             are managed centrally in lib/pom.xml. -->
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-resolver-provider</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven.resolver</groupId>
+            <artifactId>maven-resolver-connector-basic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven.resolver</groupId>
+            <artifactId>maven-resolver-transport-file</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven.resolver</groupId>
+            <artifactId>maven-resolver-transport-http</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.eclipse.jetty</groupId>
             <artifactId>jetty-security</artifactId>
diff --git a/engine/src/main/java/org/apache/hop/driver/DriverCatalog.java b/engine/src/main/java/org/apache/hop/driver/DriverCatalog.java
new file mode 100644
index 0000000000..34b5ce8b0a
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/driver/DriverCatalog.java
@@ -0,0 +1,119 @@
+/*
+ * 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
+ *
+ *       http://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.hop.driver;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.hop.core.database.DatabasePluginType;
+import org.apache.hop.core.database.DriverDownload;
+import org.apache.hop.core.database.IDatabase;
+import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.PluginRegistry;
+
+/**
+ * The catalog of downloadable JDBC drivers. There is no separate catalog file: this aggregates the
+ * {@link DriverDownload} descriptors that the database plugins declare from {@link
+ * IDatabase#getDriverDownload()}, so the driver definition lives next to the database metadata and
+ * external plugins automatically contribute their own.
+ */
+public class DriverCatalog {
+
+  private final Map<String, DriverDefinition> driversById = new LinkedHashMap<>();
+
+  private DriverCatalog() {
+    // use load()
+  }
+
+  /** Build the catalog by scanning all registered database plugins for a driver download. */
+  public static DriverCatalog load() {
+    DriverCatalog catalog = new DriverCatalog();
+    PluginRegistry registry = PluginRegistry.getInstance();
+    for (IPlugin plugin : registry.getPlugins(DatabasePluginType.class)) {
+      try {
+        Object loaded = registry.loadClass(plugin);
+        if (loaded instanceof IDatabase database) {
+          DriverDownload download = database.getDriverDownload();
+          if (download != null) {
+            DriverDefinition definition =
+                new DriverDefinition(
+                    plugin.getIds()[0], plugin.getName(), driverClass(database), download);
+            catalog.driversById.put(definition.getId(), definition);
+          }
+        }
+      } catch (Exception e) {
+        // Skip any plugin that fails to load; it simply has no downloadable driver here.
+      }
+    }
+    return catalog;
+  }
+
+  private static String driverClass(IDatabase database) {
+    try {
+      return database.getDriverClass();
+    } catch (Exception e) {
+      return null;
+    }
+  }
+
+  /**
+   * @return all downloadable drivers, in plugin-registration order.
+   */
+  public List<DriverDefinition> list() {
+    return new ArrayList<>(driversById.values());
+  }
+
+  /**
+   * @return the entry for the given id/database type (case-insensitive), or null if not found.
+   */
+  public DriverDefinition get(String id) {
+    return id == null ? null : driversById.get(id.toLowerCase(Locale.ROOT));
+  }
+
+  /**
+   * Find a catalog entry by Hop database plugin type, e.g. {@code ORACLE} (matches {@code
+   * DatabaseMeta.getPluginId()}). The id and the database type share the same key space.
+   */
+  public DriverDefinition getByDatabaseType(String databaseType) {
+    return get(databaseType);
+  }
+
+  /**
+   * Best-effort check whether a jar for this driver is already present in the given lib/jdbc
+   * folder. Heuristic: a file name in the folder starts with the driver's Maven artifactId.
+   */
+  public static boolean isInstalled(DriverDefinition driver, File libJdbcFolder) {
+    String artifactId = driver.getDownload() == null ? null : driver.getDownload().getArtifactId();
+    if (artifactId == null || libJdbcFolder == null || !libJdbcFolder.isDirectory()) {
+      return false;
+    }
+    File[] jars = libJdbcFolder.listFiles((dir, fileName) -> fileName.endsWith(".jar"));
+    if (jars == null) {
+      return false;
+    }
+    for (File jar : jars) {
+      if (jar.getName().startsWith(artifactId)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/engine/src/main/java/org/apache/hop/driver/DriverDefinition.java b/engine/src/main/java/org/apache/hop/driver/DriverDefinition.java
new file mode 100644
index 0000000000..c018d0b23a
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/driver/DriverDefinition.java
@@ -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
+ *
+ *       http://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.hop.driver;
+
+import java.util.Locale;
+import lombok.Getter;
+import org.apache.hop.core.database.DriverDownload;
+
+/**
+ * A catalog entry combining a database plugin (its type, name and JDBC driver class) with the
+ * {@link DriverDownload} descriptor that plugin declares. Assembled by {@link DriverCatalog} from
+ * the database plugins - there is no separate catalog file; the driver download lives next to the
+ * rest of the database metadata, so external plugins can contribute their own.
+ */
+@Getter
+public class DriverDefinition {
+
+  /** Command-line id: the database type lowercased, e.g. {@code oracle}. */
+  private final String id;
+
+  /** The Hop database plugin type, e.g. {@code ORACLE}. */
+  private final String databaseType;
+
+  /** The database plugin's display name. */
+  private final String name;
+
+  /** The JDBC driver class, e.g. {@code oracle.jdbc.OracleDriver}. */
+  private final String driverClass;
+
+  /** The download descriptor declared by the database plugin. */
+  private final DriverDownload download;
+
+  public DriverDefinition(
+      String databaseType, String name, String driverClass, DriverDownload download) {
+    this.databaseType = databaseType;
+    this.id = databaseType == null ? null : databaseType.toLowerCase(Locale.ROOT);
+    this.name = name;
+    this.driverClass = driverClass;
+    this.download = download;
+  }
+
+  /**
+   * @return true when the driver is restricted (Category X) and must not be bundled.
+   */
+  public boolean isRestricted() {
+    return download != null && download.isRestricted();
+  }
+
+  /**
+   * @return the full Maven coordinate, honouring an optional version override.
+   */
+  public String toCoordinate(String version) {
+    return download == null ? null : download.toCoordinate(version);
+  }
+}
diff --git a/engine/src/main/java/org/apache/hop/driver/DriverInstallCommand.java b/engine/src/main/java/org/apache/hop/driver/DriverInstallCommand.java
new file mode 100644
index 0000000000..2f384dad13
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/driver/DriverInstallCommand.java
@@ -0,0 +1,184 @@
+/*
+ * 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
+ *
+ *       http://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.hop.driver;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.Callable;
+import org.apache.hop.core.exception.HopException;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
+
+/**
+ * {@code hop driver install <id>}: download a JDBC driver (and its transitive runtime dependencies)
+ * and install it into lib/jdbc.
+ *
+ * <p>For restricted (Category X) drivers the install is refused unless {@code --accept-license} is
+ * given: Apache Hop neither ships nor hosts these drivers; you obtain them from the vendor / Maven
+ * Central under the vendor's license, at your own request.
+ */
+@SuppressWarnings("java:S106")
+@Command(
+    name = "install",
+    description = "Download and install a JDBC driver into the configured JDBC folder",
+    mixinStandardHelpOptions = true)
+public class DriverInstallCommand implements Callable<Integer> {
+
+  private static final int EXIT_OK = 0;
+  private static final int EXIT_LICENSE_REQUIRED = 2;
+  private static final int EXIT_UNKNOWN_DRIVER = 3;
+
+  @Parameters(
+      index = "0",
+      paramLabel = "<driver-id>",
+      description = "Driver id from 'hop driver list' (e.g. oracle, mysql, mariadb, db2)")
+  private String driverId;
+
+  @Option(
+      names = {"--driver-version"},
+      description = "Version to install (default: the catalog's default version)")
+  private String version;
+
+  @Option(
+      names = {"--accept-license"},
+      description = "Accept the vendor license. Required for restricted (LIC=X) drivers.")
+  private boolean acceptLicense;
+
+  @Option(
+      names = {"--repo"},
+      description = "Maven repository base URL to download from (default: Maven Central)")
+  private String repoUrl;
+
+  @Option(
+      names = {"--target"},
+      description =
+          "Target folder to install into "
+              + "(default: the first non-lib/jdbc HOP_SHARED_JDBC_FOLDERS entry, else lib/jdbc)")
+  private String targetFolder;
+
+  @Option(
+      names = {"--force"},
+      description = "Re-download and re-install even if the same version is already installed.")
+  private boolean force;
+
+  @Override
+  public Integer call() throws HopException {
+    DriverCatalog catalog = DriverCatalog.load();
+    DriverDefinition driver = catalog.get(driverId);
+    if (driver == null) {
+      System.err.println("Unknown driver id: '" + driverId + "'.");
+      System.err.println("Run 'hop driver list' to see the available drivers.");
+      return EXIT_UNKNOWN_DRIVER;
+    }
+
+    if (driver.isRestricted() && !acceptLicense) {
+      printLicenseNotice(driver);
+      return EXIT_LICENSE_REQUIRED;
+    }
+
+    File target =
+        (targetFolder != null && !targetFolder.isBlank())
+            ? new File(targetFolder).getAbsoluteFile()
+            : DriverInstaller.defaultInstallFolder();
+
+    String installVersion =
+        (version != null && !version.isBlank())
+            ? version
+            : driver.getDownload().getDefaultVersion();
+    String artifactId = driver.getDownload().getArtifactId();
+    List<DriverInstaller.InstalledDriver> existing = DriverInstaller.findInstalled(artifactId);
+
+    boolean sameVersionInTarget =
+        existing.stream()
+            .anyMatch(p -> p.folder().equals(target) && p.version().equals(installVersion));
+    if (sameVersionInTarget && !force) {
+      System.out.println(
+          driver.getName() + " " + installVersion + " is already installed in " + target + ".");
+      System.out.println("Use --force to download and re-install it.");
+      return EXIT_OK;
+    }
+
+    // Warn about copies in other scanned folders (e.g. the bundled lib/jdbc) that are left in
+    // place.
+    for (DriverInstaller.InstalledDriver present : existing) {
+      if (!present.folder().equals(target)) {
+        System.out.println(
+            "Note: "
+                + driver.getName()
+                + " "
+                + present.version()
+                + " is also present in "
+                + present.folder()
+                + "; it will not be removed, so you may want to delete it to avoid version conflicts.");
+      }
+    }
+
+    String coordinate = driver.toCoordinate(version);
+    System.out.println("Installing driver '" + driver.getId() + "' (" + driver.getName() + ")");
+    System.out.println("  artifact : " + coordinate);
+    System.out.println(
+        "  license  : "
+            + driver.getDownload().getLicenseName()
+            + " (category "
+            + driver.getDownload().getLicenseCategory()
+            + ")");
+    System.out.println(
+        "  from     : " + (repoUrl != null ? repoUrl : DriverResolver.MAVEN_CENTRAL));
+    System.out.println("  into     : " + target);
+    System.out.println();
+
+    List<File> installed =
+        new DriverInstaller().install(driver.getDownload(), version, repoUrl, target);
+
+    System.out.println("Installed " + installed.size() + " jar(s):");
+    for (File file : installed) {
+      System.out.println("  - " + file.getName());
+    }
+    System.out.println();
+    System.out.println("Restart Hop (or the server/worker) so the driver is picked up.");
+    return EXIT_OK;
+  }
+
+  private void printLicenseNotice(DriverDefinition driver) {
+    System.out.println();
+    System.out.println(
+        "The "
+            + driver.getName()
+            + " JDBC driver is licensed under "
+            + driver.getDownload().getLicenseName()
+            + " by "
+            + driver.getDownload().getVendor()
+            + ".");
+    System.out.println("It is NOT distributed with Apache Hop.");
+    System.out.println();
+    System.out.println("  Driver   : " + driver.toCoordinate(version));
+    System.out.println("  License  : " + driver.getDownload().getLicenseUrl());
+    System.out.println("  Vendor   : " + driver.getDownload().getVendorUrl());
+    if (driver.getDownload().getNotes() != null) {
+      System.out.println("  Notes    : " + driver.getDownload().getNotes());
+    }
+    System.out.println();
+    System.out.println(
+        "If you accept the vendor's license, Hop can download it from Maven Central onto this");
+    System.out.println("machine only. Re-run with --accept-license:");
+    System.out.println();
+    System.out.println("  hop driver install " + driver.getId() + " --accept-license");
+    System.out.println();
+  }
+}
diff --git a/engine/src/main/java/org/apache/hop/driver/DriverInstaller.java b/engine/src/main/java/org/apache/hop/driver/DriverInstaller.java
new file mode 100644
index 0000000000..7da18f44a4
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/driver/DriverInstaller.java
@@ -0,0 +1,245 @@
+/*
+ * 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
+ *
+ *       http://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.hop.driver;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.database.DatabasePluginType;
+import org.apache.hop.core.database.DriverDownload;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.plugins.HopURLClassLoader;
+import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.driver.DriverResolver.ResolvedArtifact;
+
+/**
+ * Downloads a driver (and its transitive runtime dependencies) and installs the resulting jars into
+ * a JDBC folder Hop scans, where the existing {@code DatabasePluginType} scan picks them up. It can
+ * also hot-load them into the database plugin's classloader so they can be used without a restart.
+ * This is the single install path the CLI, the Studio dialog and the Docker entrypoint all call.
+ */
+public class DriverInstaller {
+
+  /**
+   * The folder where Hop's own (bundled) JDBC drivers live. Downloaded drivers should not be mixed
+   * in here, so they survive upgrades and don't touch the shipped distribution.
+   */
+  public static final String BUNDLED_JDBC_FOLDER = "lib/jdbc";
+
+  /** A driver jar found in one of the scanned JDBC folders. */
+  public record InstalledDriver(File folder, File jar, String version) {}
+
+  /**
+   * All folders Hop scans for JDBC drivers: the entries of {@code HOP_SHARED_JDBC_FOLDERS}, or just
+   * {@code lib/jdbc} when it is not set.
+   */
+  public static List<File> configuredJdbcFolders() {
+    List<File> folders = new ArrayList<>();
+    String shared = System.getProperty(Const.HOP_SHARED_JDBC_FOLDERS);
+    if (shared != null && !shared.isBlank()) {
+      for (String entry : shared.split(",")) {
+        String folder = entry.trim();
+        if (!folder.isEmpty()) {
+          folders.add(new File(folder).getAbsoluteFile());
+        }
+      }
+    }
+    if (folders.isEmpty()) {
+      folders.add(new File(BUNDLED_JDBC_FOLDER).getAbsoluteFile());
+    }
+    return folders;
+  }
+
+  /**
+   * The folder a downloaded driver should be installed into. To keep downloaded (and especially
+   * proprietary) drivers out of Hop's own bundled {@code lib/jdbc} folder, this returns the first
+   * entry of {@code HOP_SHARED_JDBC_FOLDERS} that is NOT {@code lib/jdbc}. It only falls back to
+   * {@code lib/jdbc} when that is the only configured folder, or nothing is configured at all.
+   */
+  public static File defaultInstallFolder() {
+    String shared = System.getProperty(Const.HOP_SHARED_JDBC_FOLDERS);
+    if (shared == null || shared.isBlank()) {
+      return new File(BUNDLED_JDBC_FOLDER).getAbsoluteFile();
+    }
+    String fallback = null;
+    for (String entry : shared.split(",")) {
+      String folder = entry.trim();
+      if (folder.isEmpty()) {
+        continue;
+      }
+      if (fallback == null) {
+        fallback = folder;
+      }
+      if (!isBundledFolder(folder)) {
+        return new File(folder).getAbsoluteFile();
+      }
+    }
+    return new File(fallback != null ? fallback : BUNDLED_JDBC_FOLDER).getAbsoluteFile();
+  }
+
+  private static boolean isBundledFolder(String folder) {
+    String normalized = folder.replace('\\', '/');
+    return normalized.equals(BUNDLED_JDBC_FOLDER) || normalized.endsWith("/" + BUNDLED_JDBC_FOLDER);
+  }
+
+  /** Local Aether cache (a Maven-style local repository). Kept on the user's machine only. */
+  public static File defaultCacheFolder() {
+    return new File(System.getProperty("user.home"), ".hop" + File.separator + "driver-cache");
+  }
+
+  /**
+   * Find every installed jar of the given Maven artifact across all scanned JDBC folders.
+   *
+   * @param artifactId the Maven artifactId, e.g. {@code ojdbc11}
+   * @return the matching jars with their version and folder, possibly empty
+   */
+  public static List<InstalledDriver> findInstalled(String artifactId) {
+    List<InstalledDriver> found = new ArrayList<>();
+    if (artifactId == null || artifactId.isBlank()) {
+      return found;
+    }
+    String prefix = artifactId + "-";
+    for (File folder : configuredJdbcFolders()) {
+      File[] jars = folder.listFiles((dir, name) -> name.endsWith(".jar"));
+      if (jars == null) {
+        continue;
+      }
+      for (File jar : jars) {
+        String version = versionOf(prefix, jar.getName());
+        if (version != null) {
+          found.add(new InstalledDriver(folder, jar, version));
+        }
+      }
+    }
+    return found;
+  }
+
+  /**
+   * Extract the version from a jar name of the form {@code <artifactId>-<version>.jar}, where the
+   * version starts with a digit. Returns null when the name does not match this artifact.
+   */
+  private static String versionOf(String prefix, String jarName) {
+    if (!jarName.startsWith(prefix)
+        || !jarName.endsWith(".jar")
+        || jarName.length() <= prefix.length()
+        || !Character.isDigit(jarName.charAt(prefix.length()))) {
+      return null;
+    }
+    return jarName.substring(prefix.length(), jarName.length() - ".jar".length());
+  }
+
+  /**
+   * Resolve + download the driver and copy every resolved jar into {@code targetFolder}. Any
+   * existing jar of the same artifact with a different version is removed from the target folder
+   * first, so the folder never ends up with two versions of the same driver (or dependency).
+   *
+   * @param download the driver download descriptor (from the database plugin)
+   * @param version optional version override (null uses the descriptor default)
+   * @param repoUrl optional Maven repository URL (null uses Maven Central)
+   * @param targetFolder the folder to install into
+   * @return the list of jar files written into the target folder
+   */
+  public List<File> install(
+      DriverDownload download, String version, String repoUrl, File targetFolder)
+      throws HopException {
+
+    File cache = defaultCacheFolder();
+    if (!cache.exists() && !cache.mkdirs()) {
+      throw new HopException("Unable to create driver cache folder " + cache.getAbsolutePath());
+    }
+
+    DriverResolver resolver = new DriverResolver(cache);
+    List<ResolvedArtifact> resolved =
+        resolver.resolve(
+            download.toCoordinate(version),
+            repoUrl,
+            download.getExcludes(),
+            download.getRepositoryUrl());
+
+    if (!targetFolder.exists() && !targetFolder.mkdirs()) {
+      throw new HopException("Unable to create target folder " + targetFolder.getAbsolutePath());
+    }
+
+    List<File> installed = new ArrayList<>();
+    for (ResolvedArtifact artifact : resolved) {
+      removeOtherVersions(targetFolder, artifact);
+      File destination = new File(targetFolder, artifact.file().getName());
+      try {
+        Files.copy(
+            artifact.file().toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
+      } catch (Exception e) {
+        throw new HopException("Unable to copy " + artifact.file() + " to " + destination, e);
+      }
+      installed.add(destination);
+    }
+    return installed;
+  }
+
+  /** Remove jars of the same artifact but a different version from the target folder. */
+  static void removeOtherVersions(File targetFolder, ResolvedArtifact artifact) {
+    File[] existing = targetFolder.listFiles((dir, name) -> name.endsWith(".jar"));
+    if (existing == null) {
+      return;
+    }
+    String prefix = artifact.artifactId() + "-";
+    String keep = artifact.file().getName();
+    for (File jar : existing) {
+      if (!jar.getName().equals(keep) && versionOf(prefix, jar.getName()) != null) {
+        // Same artifact, different version: remove it so we don't keep two versions.
+        try {
+          Files.deleteIfExists(jar.toPath());
+        } catch (Exception e) {
+          // Best effort; leave the old jar in place if it cannot be removed.
+        }
+      }
+    }
+  }
+
+  /**
+   * Make freshly-installed jars usable without a restart by injecting them into the database
+   * plugin's classloader. The database plugin classloaders are otherwise fixed at startup, so a
+   * driver dropped into a JDBC folder would only be picked up on the next launch.
+   *
+   * @param databaseType the database plugin type, e.g. {@code ORACLE}
+   * @param jars the jar files to add (typically the result of {@link #install})
+   * @return true if the jars were hot-loaded into the plugin classloader
+   */
+  public static boolean hotLoad(String databaseType, List<File> jars) {
+    try {
+      PluginRegistry registry = PluginRegistry.getInstance();
+      IPlugin plugin = registry.getPlugin(DatabasePluginType.class, databaseType);
+      if (plugin == null) {
+        return false;
+      }
+      ClassLoader classLoader = registry.getClassLoader(plugin);
+      if (classLoader instanceof HopURLClassLoader hopClassLoader) {
+        for (File jar : jars) {
+          hopClassLoader.addJar(jar.toURI().toURL());
+        }
+        return true;
+      }
+      return false;
+    } catch (Exception e) {
+      return false;
+    }
+  }
+}
diff --git a/engine/src/main/java/org/apache/hop/driver/DriverListCommand.java b/engine/src/main/java/org/apache/hop/driver/DriverListCommand.java
new file mode 100644
index 0000000000..0509b66046
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/driver/DriverListCommand.java
@@ -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
+ *
+ *       http://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.hop.driver;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.Callable;
+import org.apache.hop.core.exception.HopException;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+/** {@code hop driver list}: print the driver catalog and each driver's install status. */
+@SuppressWarnings("java:S106")
+@Command(
+    name = "list",
+    description = "List the JDBC drivers in the catalog and their install status",
+    mixinStandardHelpOptions = true)
+public class DriverListCommand implements Callable<Integer> {
+
+  @Option(
+      names = {"--target"},
+      description =
+          "Only report install status for this folder "
+              + "(default: all HOP_SHARED_JDBC_FOLDERS entries, else lib/jdbc)")
+  private String targetFolder;
+
+  @Override
+  public Integer call() throws HopException {
+    DriverCatalog catalog = DriverCatalog.load();
+    List<File> folders =
+        (targetFolder != null && !targetFolder.isBlank())
+            ? List.of(new File(targetFolder).getAbsoluteFile())
+            : DriverInstaller.configuredJdbcFolders();
+
+    System.out.println("JDBC drivers declared by the database plugins");
+    System.out.println("Scanned folders: " + folders);
+    System.out.println();
+    String format = "%-12s %-26s %-5s %-10s %s%n";
+    System.out.printf(format, "ID", "DATABASE", "LIC", "INSTALLED", "ARTIFACT:VERSION");
+    System.out.printf(format, "----", "--------", "---", "---------", "----------------");
+
+    for (DriverDefinition driver : catalog.list()) {
+      boolean installed =
+          folders.stream().anyMatch(folder -> DriverCatalog.isInstalled(driver, folder));
+      System.out.printf(
+          format,
+          driver.getId(),
+          truncate(driver.getName(), 26),
+          driver.getDownload().getLicenseCategory(),
+          installed ? "yes" : "no",
+          driver.toCoordinate(null));
+    }
+
+    System.out.println();
+    System.out.println("Install a driver with:");
+    System.out.println("  hop driver install <id> [--accept-license]");
+    System.out.println("--accept-license is required for restricted (LIC=X) drivers.");
+    return 0;
+  }
+
+  private static String truncate(String value, int max) {
+    if (value == null) {
+      return "";
+    }
+    return value.length() <= max ? value : value.substring(0, max - 1) + "…";
+  }
+}
diff --git a/engine/src/main/java/org/apache/hop/driver/DriverResolver.java b/engine/src/main/java/org/apache/hop/driver/DriverResolver.java
new file mode 100644
index 0000000000..c9376ccf94
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/driver/DriverResolver.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
+ *
+ *       http://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.hop.driver;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.apache.hop.core.exception.HopException;
+import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.Exclusion;
+import org.eclipse.aether.impl.DefaultServiceLocator;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.DependencyRequest;
+import org.eclipse.aether.resolution.DependencyResult;
+import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.transport.file.FileTransporterFactory;
+import org.eclipse.aether.transport.http.HttpTransporterFactory;
+import org.eclipse.aether.util.artifact.JavaScopes;
+import org.eclipse.aether.util.filter.DependencyFilterUtils;
+import org.eclipse.aether.util.graph.selector.AndDependencySelector;
+import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector;
+import org.eclipse.aether.util.graph.selector.ScopeDependencySelector;
+
+/**
+ * Resolves a Maven coordinate (and its transitive runtime dependencies) to a set of jar files,
+ * downloading from a remote repository into a local cache. Wraps the Maven Artifact Resolver
+ * (ex-Eclipse Aether).
+ *
+ * <p>ASF note: this never bundles anything. It downloads, at the caller's explicit request, from
+ * Maven Central (or a user-supplied repository) straight onto the user's machine.
+ */
+public class DriverResolver {
+
+  /** Maven Central. Deliberately NOT an ASF- or Hop-operated mirror (ASF policy on Cat-X bytes). */
+  public static final String MAVEN_CENTRAL = "https://repo1.maven.org/maven2/";
+
+  private final File localCacheDir;
+
+  public DriverResolver(File localCacheDir) {
+    this.localCacheDir = localCacheDir;
+  }
+
+  /** A resolved jar: its Maven artifactId, version and the downloaded file on disk. */
+  public record ResolvedArtifact(String artifactId, String version, File file) {}
+
+  /**
+   * Resolve a coordinate to the jar plus its transitive runtime dependency jars, downloading as
+   * needed.
+   *
+   * @param coordinate {@code groupId:artifactId:version}
+   * @param remoteRepoUrl primary repository base URL, or null for Maven Central
+   * @param excludes Maven coordinates ({@code groupId:artifactId}, artifactId may be {@code *}) to
+   *     exclude, e.g. dependencies Hop already ships in lib/core. Optional and test/provided
+   *     dependencies are always skipped.
+   * @param driverRepoUrl optional extra repository to search (for drivers not on Maven Central),
+   *     searched in addition to the primary repository; null to use only the primary
+   * @return the resolved artifacts: the driver plus its (non-optional) transitive runtime
+   *     dependencies, minus the exclusions
+   */
+  public List<ResolvedArtifact> resolve(
+      String coordinate, String remoteRepoUrl, List<String> excludes, String driverRepoUrl)
+      throws HopException {
+    try {
+      RepositorySystem system = newRepositorySystem();
+      DefaultRepositorySystemSession session = newSession(system);
+
+      List<RemoteRepository> repositories = new ArrayList<>();
+      repositories.add(
+          new RemoteRepository.Builder(
+                  "primary",
+                  "default",
+                  (remoteRepoUrl == null || remoteRepoUrl.isBlank())
+                      ? MAVEN_CENTRAL
+                      : remoteRepoUrl)
+              .build());
+      if (driverRepoUrl != null && !driverRepoUrl.isBlank()) {
+        repositories.add(
+            new RemoteRepository.Builder("driver-repo", "default", driverRepoUrl).build());
+      }
+
+      Artifact artifact = new DefaultArtifact(coordinate);
+      CollectRequest collectRequest = new CollectRequest();
+      collectRequest.setRoot(
+          new Dependency(artifact, JavaScopes.RUNTIME, false, toExclusions(excludes)));
+      collectRequest.setRepositories(repositories);
+
+      DependencyRequest dependencyRequest =
+          new DependencyRequest(
+              collectRequest, DependencyFilterUtils.classpathFilter(JavaScopes.RUNTIME));
+
+      DependencyResult result = system.resolveDependencies(session, dependencyRequest);
+
+      List<ResolvedArtifact> artifacts = new ArrayList<>();
+      for (ArtifactResult artifactResult : result.getArtifactResults()) {
+        Artifact resolved = artifactResult.getArtifact();
+        if (resolved.getFile() != null) {
+          artifacts.add(
+              new ResolvedArtifact(
+                  resolved.getArtifactId(), resolved.getVersion(), resolved.getFile()));
+        }
+      }
+      return artifacts;
+    } catch (Exception e) {
+      throw new HopException("Unable to resolve driver artifact '" + coordinate + "'", e);
+    }
+  }
+
+  @SuppressWarnings("deprecation") // ServiceLocator is the simplest bootstrap on resolver 1.9.x
+  private static RepositorySystem newRepositorySystem() {
+    DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
+    locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
+    locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
+    locator.addService(TransporterFactory.class, FileTransporterFactory.class);
+    return locator.getService(RepositorySystem.class);
+  }
+
+  private DefaultRepositorySystemSession newSession(RepositorySystem system) {
+    DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
+    LocalRepository localRepo = new LocalRepository(localCacheDir);
+    session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));
+    // Skip optional and test/provided dependencies, and honour per-dependency exclusions. This is
+    // the Maven default selector with OptionalDependencySelector replaced so that even direct
+    // optional dependencies (e.g. a driver's Windows-only auth lib) are dropped.
+    session.setDependencySelector(
+        new AndDependencySelector(
+            new ScopeDependencySelector(JavaScopes.TEST, JavaScopes.PROVIDED),
+            new SkipOptionalDependencySelector(),
+            new ExclusionDependencySelector()));
+    return session;
+  }
+
+  /**
+   * Build Aether exclusions from {@code groupId:artifactId} strings (artifactId may be {@code *}).
+   *
+   * <p>Package-private so the parsing can be unit tested without resolving anything over the
+   * network.
+   */
+  static Collection<Exclusion> toExclusions(List<String> excludes) {
+    if (excludes == null || excludes.isEmpty()) {
+      return Collections.emptyList();
+    }
+    List<Exclusion> exclusions = new ArrayList<>();
+    for (String exclude : excludes) {
+      if (exclude == null || exclude.isBlank()) {
+        continue;
+      }
+      String[] parts = exclude.split(":");
+      String groupId = parts[0].trim();
+      String artifactId = parts.length > 1 ? parts[1].trim() : "*";
+      exclusions.add(new Exclusion(groupId, artifactId, "*", "*"));
+    }
+    return exclusions;
+  }
+
+  /** A dependency selector that drops every optional dependency, at any depth. */
+  private static class SkipOptionalDependencySelector implements DependencySelector {
+    @Override
+    public boolean selectDependency(Dependency dependency) {
+      return !dependency.isOptional();
+    }
+
+    @Override
+    public DependencySelector deriveChildSelector(DependencyCollectionContext context) {
+      return this;
+    }
+  }
+}
diff --git a/engine/src/main/java/org/apache/hop/driver/HopDriver.java b/engine/src/main/java/org/apache/hop/driver/HopDriver.java
new file mode 100644
index 0000000000..677ba9090f
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/driver/HopDriver.java
@@ -0,0 +1,63 @@
+/*
+ * 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
+ *
+ *       http://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.hop.driver;
+
+import org.apache.hop.core.HopVersionProvider;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.hop.plugin.HopCommand;
+import org.apache.hop.hop.plugin.IHopCommand;
+import org.apache.hop.metadata.serializer.multi.MultiMetadataProvider;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Model.CommandSpec;
+import picocli.CommandLine.Spec;
+
+/**
+ * The {@code hop driver} command: manage JDBC drivers. Holds the {@code list} and {@code install}
+ * subcommands.
+ *
+ * <p>Apache Hop does not bundle restricted (proprietary / GPL / LGPL) JDBC drivers. This command
+ * downloads them from Maven Central onto this machine, at your explicit request, after you accept
+ * the vendor license. See {@code hop driver install --help}.
+ */
+@SuppressWarnings("java:S106")
+@Command(
+    name = "driver",
+    description = "Manage JDBC drivers (list, install)",
+    versionProvider = HopVersionProvider.class,
+    mixinStandardHelpOptions = true,
+    subcommands = {DriverListCommand.class, DriverInstallCommand.class})
+@HopCommand(id = "driver", description = "Manage JDBC drivers (list, install)")
+public class HopDriver implements IHopCommand, Runnable {
+
+  @Spec private CommandSpec spec;
+
+  @Override
+  public void initialize(
+      CommandLine cmd, IVariables variables, MultiMetadataProvider metadataProvider)
+      throws HopException {
+    // Nothing to initialize: the subcommands are self-contained.
+  }
+
+  @Override
+  public void run() {
+    // No subcommand given: show usage.
+    spec.commandLine().usage(System.out);
+  }
+}
diff --git a/engine/src/test/java/org/apache/hop/driver/DriverDefinitionTest.java b/engine/src/test/java/org/apache/hop/driver/DriverDefinitionTest.java
new file mode 100644
index 0000000000..762177c91c
--- /dev/null
+++ b/engine/src/test/java/org/apache/hop/driver/DriverDefinitionTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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
+ *
+ *       http://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.hop.driver;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.database.DriverDownload;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link DriverDefinition}, the catalog entry that pairs a database plugin with the
+ * {@link DriverDownload} it declares. Pure logic, no plugin registry or network needed.
+ */
+class DriverDefinitionTest {
+
+  private static DriverDownload restricted() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.oracle.database.jdbc:ojdbc11")
+        .defaultVersion("23.5.0.24.07")
+        .licenseCategory("X")
+        .build();
+  }
+
+  @Test
+  void idIsTheDatabaseTypeLowercased() {
+    DriverDownload download = restricted();
+    DriverDefinition def =
+        new DriverDefinition("ORACLE", "Oracle", "oracle.jdbc.OracleDriver", download);
+
+    assertEquals("oracle", def.getId());
+    assertEquals("ORACLE", def.getDatabaseType());
+    assertEquals("Oracle", def.getName());
+    assertEquals("oracle.jdbc.OracleDriver", def.getDriverClass());
+    assertSame(download, def.getDownload(), "the declared descriptor is exposed as-is");
+  }
+
+  @Test
+  void nullDatabaseTypeYieldsNullId() {
+    DriverDefinition def = new DriverDefinition(null, "Nameless", "no.Driver", null);
+    assertNull(def.getId(), "a null database type must not throw and must give a null id");
+  }
+
+  @Test
+  void restrictedFollowsTheDownloadCategory() {
+    assertTrue(
+        new DriverDefinition("ORACLE", "Oracle", "oracle.jdbc.OracleDriver", restricted())
+            .isRestricted());
+
+    DriverDownload open =
+        DriverDownload.builder()
+            .mavenCoordinate("org.postgresql:postgresql")
+            .defaultVersion("42.7.4")
+            .licenseCategory("A")
+            .build();
+    assertFalse(
+        new DriverDefinition("POSTGRESQL", "PostgreSQL", "org.postgresql.Driver", open)
+            .isRestricted());
+  }
+
+  @Test
+  void withoutADownloadItIsNeitherRestrictedNorResolvable() {
+    DriverDefinition def = new DriverDefinition("GENERIC", "Generic", "some.Driver", null);
+    assertFalse(def.isRestricted(), "no descriptor -> not restricted");
+    assertNull(def.toCoordinate(null), "no descriptor -> no coordinate to resolve");
+  }
+
+  @Test
+  void coordinateDelegatesToTheDownloadAndHonoursOverride() {
+    DriverDefinition def =
+        new DriverDefinition("ORACLE", "Oracle", "oracle.jdbc.OracleDriver", restricted());
+
+    assertEquals("com.oracle.database.jdbc:ojdbc11:23.5.0.24.07", def.toCoordinate(null));
+    assertEquals("com.oracle.database.jdbc:ojdbc11:21.0.0", def.toCoordinate("21.0.0"));
+  }
+}
diff --git a/engine/src/test/java/org/apache/hop/driver/DriverInstallerTest.java b/engine/src/test/java/org/apache/hop/driver/DriverInstallerTest.java
new file mode 100644
index 0000000000..303a861f31
--- /dev/null
+++ b/engine/src/test/java/org/apache/hop/driver/DriverInstallerTest.java
@@ -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
+ *
+ *       http://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.hop.driver;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.List;
+import org.apache.hop.core.Const;
+import org.apache.hop.driver.DriverResolver.ResolvedArtifact;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class DriverInstallerTest {
+
+  private String original;
+
+  @BeforeEach
+  void capture() {
+    original = System.getProperty(Const.HOP_SHARED_JDBC_FOLDERS);
+  }
+
+  @AfterEach
+  void restore() {
+    if (original == null) {
+      System.clearProperty(Const.HOP_SHARED_JDBC_FOLDERS);
+    } else {
+      System.setProperty(Const.HOP_SHARED_JDBC_FOLDERS, original);
+    }
+  }
+
+  private static void setFolders(String value) {
+    if (value == null) {
+      System.clearProperty(Const.HOP_SHARED_JDBC_FOLDERS);
+    } else {
+      System.setProperty(Const.HOP_SHARED_JDBC_FOLDERS, value);
+    }
+  }
+
+  @Test
+  void installFolderDefaultsToLibJdbcWhenUnset() {
+    setFolders(null);
+    assertEquals(new File("lib/jdbc").getAbsoluteFile(), DriverInstaller.defaultInstallFolder());
+  }
+
+  @Test
+  void installFolderSkipsLibJdbcWhenAnotherFolderIsConfigured() {
+    setFolders("lib/jdbc,some/other/folder");
+    assertEquals(
+        new File("some/other/folder").getAbsoluteFile(), DriverInstaller.defaultInstallFolder());
+  }
+
+  @Test
+  void installFolderPicksFirstNonLibJdbcRegardlessOfOrder() {
+    setFolders("custom/a,lib/jdbc");
+    assertEquals(new File("custom/a").getAbsoluteFile(), DriverInstaller.defaultInstallFolder());
+  }
+
+  @Test
+  void installFolderFallsBackToLibJdbcWhenItIsTheOnlyEntry() {
+    setFolders("lib/jdbc");
+    assertEquals(new File("lib/jdbc").getAbsoluteFile(), DriverInstaller.defaultInstallFolder());
+  }
+
+  @Test
+  void installFolderTreatsAbsoluteLibJdbcAsBundled() {
+    setFolders("/opt/hop/lib/jdbc,/opt/hop/extra-drivers");
+    assertEquals(
+        new File("/opt/hop/extra-drivers").getAbsoluteFile(),
+        DriverInstaller.defaultInstallFolder());
+  }
+
+  @Test
+  void findInstalledReturnsOnlyMatchingArtifactVersions(@TempDir File dir) throws Exception {
+    Files.createFile(new File(dir, "ojdbc11-23.5.0.24.07.jar").toPath());
+    Files.createFile(new File(dir, "ojdbc11-21.0.0.jar").toPath());
+    Files.createFile(new File(dir, "ojdbc8-1.0.jar").toPath()); // different artifact
+    Files.createFile(new File(dir, "ojdbc11-notes.txt").toPath()); // not a jar
+    setFolders(dir.getAbsolutePath());
+
+    List<DriverInstaller.InstalledDriver> found = DriverInstaller.findInstalled("ojdbc11");
+
+    assertEquals(2, found.size());
+    assertTrue(found.stream().anyMatch(d -> "23.5.0.24.07".equals(d.version())));
+    assertTrue(found.stream().anyMatch(d -> "21.0.0".equals(d.version())));
+  }
+
+  @Test
+  void removeOtherVersionsDeletesDifferentVersionsOnly(@TempDir File dir) throws Exception {
+    File oldVersion = new File(dir, "ojdbc11-21.0.0.jar");
+    File unrelated = new File(dir, "unrelated-1.0.jar");
+    File keep = new File(dir, "ojdbc11-23.5.0.24.07.jar");
+    Files.createFile(oldVersion.toPath());
+    Files.createFile(unrelated.toPath());
+    Files.createFile(keep.toPath());
+
+    DriverInstaller.removeOtherVersions(dir, new ResolvedArtifact("ojdbc11", "23.5.0.24.07", keep));
+
+    assertFalse(oldVersion.exists(), "the old version should be removed");
+    assertTrue(keep.exists(), "the version being installed should be kept");
+    assertTrue(unrelated.exists(), "unrelated jars must be left alone");
+  }
+}
diff --git a/engine/src/test/java/org/apache/hop/driver/DriverResolverTest.java b/engine/src/test/java/org/apache/hop/driver/DriverResolverTest.java
new file mode 100644
index 0000000000..800294cd96
--- /dev/null
+++ b/engine/src/test/java/org/apache/hop/driver/DriverResolverTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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
+ *
+ *       http://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.hop.driver;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import org.eclipse.aether.graph.Exclusion;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link DriverResolver#toExclusions}, the {@code groupId:artifactId} exclude
+ * parsing a database plugin can declare (e.g. to avoid pulling a jar already shipped in lib/core).
+ * No network resolution involved.
+ */
+class DriverResolverTest {
+
+  @Test
+  void nullOrEmptyExcludesProduceNoExclusions() {
+    assertTrue(DriverResolver.toExclusions(null).isEmpty());
+    assertTrue(DriverResolver.toExclusions(List.of()).isEmpty());
+  }
+
+  @Test
+  void groupAndArtifactAreParsed() {
+    Collection<Exclusion> exclusions =
+        DriverResolver.toExclusions(List.of("com.google.protobuf:protobuf-java"));
+
+    assertEquals(1, exclusions.size());
+    Exclusion exclusion = exclusions.iterator().next();
+    assertEquals("com.google.protobuf", exclusion.getGroupId());
+    assertEquals("protobuf-java", exclusion.getArtifactId());
+    assertEquals("*", exclusion.getClassifier());
+    assertEquals("*", exclusion.getExtension());
+  }
+
+  @Test
+  void artifactWildcardIsHonoured() {
+    Exclusion exclusion = DriverResolver.toExclusions(List.of("org.slf4j:*")).iterator().next();
+    assertEquals("org.slf4j", exclusion.getGroupId());
+    assertEquals("*", exclusion.getArtifactId());
+  }
+
+  @Test
+  void aGroupOnlyEntryExcludesEveryArtifactInThatGroup() {
+    Exclusion exclusion =
+        DriverResolver.toExclusions(List.of("com.google.guava")).iterator().next();
+    assertEquals("com.google.guava", exclusion.getGroupId());
+    assertEquals("*", exclusion.getArtifactId(), "a missing artifactId defaults to the wildcard");
+  }
+
+  @Test
+  void blankAndNullEntriesAreSkippedButValidOnesKept() {
+    Collection<Exclusion> exclusions =
+        DriverResolver.toExclusions(
+            Arrays.asList("  ", null, "com.example:a", "", "com.example:b"));
+
+    assertEquals(2, exclusions.size());
+  }
+}
diff --git a/integration-tests/scripts/run-tests.sh b/integration-tests/scripts/run-tests.sh
index 5d9e5305c3..4c465295fa 100755
--- a/integration-tests/scripts/run-tests.sh
+++ b/integration-tests/scripts/run-tests.sh
@@ -31,6 +31,31 @@ if [ -z "${SUREFIRE_REPORT}" ]; then
   SUREFIRE_REPORT="true"
 fi
 
+# Install any JDBC drivers required by this test set, using the Hop driver-download CLI.
+# Driven by HOP_DRIVERS_DOWNLOAD (comma separated driver ids, each optionally with a version, e.g.
+# "vertica,mysql:9.2.0"), set per test in the integration-tests-*.yaml compose files. Restricted
+# (Category X) drivers need HOP_DRIVERS_ACCEPT_LICENSE=true (the default here for the test run).
+if [ -n "${HOP_DRIVERS_DOWNLOAD}" ]; then
+  ACCEPT_FLAG=""
+  case "${HOP_DRIVERS_ACCEPT_LICENSE:-true}" in
+  true | TRUE | True | Y | y | yes | YES | 1) ACCEPT_FLAG="--accept-license" ;;
+  *) ;;
+  esac
+  for DRIVER_SPEC in ${HOP_DRIVERS_DOWNLOAD//,/ }; do
+    DRIVER_ID="${DRIVER_SPEC%%:*}"
+    VERSION_FLAG=""
+    if [ "${DRIVER_SPEC}" != "${DRIVER_ID}" ]; then
+      VERSION_FLAG="--driver-version=${DRIVER_SPEC#*:}"
+    fi
+    echo "Installing JDBC driver for the tests: ${DRIVER_SPEC}"
+    # shellcheck disable=SC2086
+    if ! bash "${HOP_LOCATION}/hop" driver install "${DRIVER_ID}" ${VERSION_FLAG} ${ACCEPT_FLAG}; then
+      echo "ERROR: failed to install JDBC driver '${DRIVER_SPEC}'"
+      exit 1
+    fi
+  done
+fi
+
 # Ensure surefire-reports directory exists and is writable
 mkdir -p "${CURRENT_DIR}"/../surefire-reports/
 chmod 777 "${CURRENT_DIR}"/../surefire-reports/ 2>/dev/null || true
diff --git a/lib/pom.xml b/lib/pom.xml
index e60480b84a..7b2a63e055 100644
--- a/lib/pom.xml
+++ b/lib/pom.xml
@@ -94,6 +94,8 @@
         <kotlin.version>1.9.25</kotlin.version>
         <log4j.version>2.25.4</log4j.version>
         <lz4-java.version>1.11.0</lz4-java.version>
+        <maven-resolver-provider.version>3.9.9</maven-resolver-provider.version>
+        <maven-resolver.version>1.9.22</maven-resolver.version>
         <msal4j-persistence.version>1.3.0</msal4j-persistence.version>
         <msal4j.version>1.16.1</msal4j.version>
         <nimbus-jose.version>10.6</nimbus-jose.version>
@@ -902,6 +904,46 @@
                 <artifactId>woodstox-core</artifactId>
                 <version>${woodstox.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.maven.resolver</groupId>
+                <artifactId>maven-resolver-api</artifactId>
+                <version>${maven-resolver.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.maven.resolver</groupId>
+                <artifactId>maven-resolver-spi</artifactId>
+                <version>${maven-resolver.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.maven.resolver</groupId>
+                <artifactId>maven-resolver-util</artifactId>
+                <version>${maven-resolver.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.maven.resolver</groupId>
+                <artifactId>maven-resolver-impl</artifactId>
+                <version>${maven-resolver.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.maven.resolver</groupId>
+                <artifactId>maven-resolver-connector-basic</artifactId>
+                <version>${maven-resolver.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.maven.resolver</groupId>
+                <artifactId>maven-resolver-transport-http</artifactId>
+                <version>${maven-resolver.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.maven.resolver</groupId>
+                <artifactId>maven-resolver-transport-file</artifactId>
+                <version>${maven-resolver.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.maven</groupId>
+                <artifactId>maven-resolver-provider</artifactId>
+                <version>${maven-resolver-provider.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 
diff --git a/plugins/databases/access/src/main/java/org/apache/hop/databases/access/AccessDatabaseMeta.java b/plugins/databases/access/src/main/java/org/apache/hop/databases/access/AccessDatabaseMeta.java
index 93e0b62a6d..36f42fffb5 100644
--- a/plugins/databases/access/src/main/java/org/apache/hop/databases/access/AccessDatabaseMeta.java
+++ b/plugins/databases/access/src/main/java/org/apache/hop/databases/access/AccessDatabaseMeta.java
@@ -17,10 +17,12 @@
 
 package org.apache.hop.databases.access;
 
+import java.util.List;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -44,6 +46,20 @@ public class AccessDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "net.ucanaccess.jdbc.UcanaccessDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("io.github.spannm:ucanaccess")
+        .defaultVersion("5.1.5")
+        .licenseCategory("A")
+        .licenseName("Apache-2.0")
+        .licenseUrl("https://github.com/spannm/ucanaccess/blob/main/LICENSE")
+        .vendor("UCanAccess (spannm fork)")
+        .vendorUrl("https://github.com/spannm/ucanaccess")
+        .excludes(List.of("org.apache.poi:poi"))
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String database) {
     return "jdbc:ucanaccess://" + database + ";showSchema=true";
diff --git a/plugins/databases/as400/src/main/java/org/apache/hop/databases/as400/AS400DatabaseMeta.java b/plugins/databases/as400/src/main/java/org/apache/hop/databases/as400/AS400DatabaseMeta.java
index bd2b7d2c8a..011ab360b4 100644
--- a/plugins/databases/as400/src/main/java/org/apache/hop/databases/as400/AS400DatabaseMeta.java
+++ b/plugins/databases/as400/src/main/java/org/apache/hop/databases/as400/AS400DatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -43,6 +44,19 @@ public class AS400DatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "com.ibm.as400.access.AS400JDBCDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("net.sf.jt400:jt400")
+        .defaultVersion("21.0.6")
+        .licenseCategory("B")
+        .licenseName("IBM Public License 1.0")
+        .licenseUrl("https://github.com/IBM/JTOpen/blob/main/LICENSE.md")
+        .vendor("IBM JTOpen (jt400)")
+        .vendorUrl("https://github.com/IBM/JTOpen")
+        .build();
+  }
+
   /**
    * Get the maximum length of a text field for this database connection. This includes optional
    * CLOB, Memo and Text fields. (the maximum!)
diff --git a/plugins/databases/clickhouse/src/main/java/org/apache/hop/databases/clickhouse/ClickhouseDatabaseMeta.java b/plugins/databases/clickhouse/src/main/java/org/apache/hop/databases/clickhouse/ClickhouseDatabaseMeta.java
index 482f179724..9b394b262b 100644
--- a/plugins/databases/clickhouse/src/main/java/org/apache/hop/databases/clickhouse/ClickhouseDatabaseMeta.java
+++ b/plugins/databases/clickhouse/src/main/java/org/apache/hop/databases/clickhouse/ClickhouseDatabaseMeta.java
@@ -18,11 +18,13 @@
 
 package org.apache.hop.databases.clickhouse;
 
+import java.util.List;
 import org.apache.commons.lang3.Validate;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -63,6 +65,20 @@ public class ClickhouseDatabaseMeta extends BaseDatabaseMeta implements IDatabas
     return "com.clickhouse.jdbc.ClickHouseDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.clickhouse:clickhouse-jdbc:jar:all")
+        .defaultVersion("0.9.8")
+        .licenseCategory("A")
+        .licenseName("Apache-2.0")
+        .licenseUrl("https://github.com/ClickHouse/clickhouse-java/blob/main/LICENSE")
+        .vendor("ClickHouse")
+        .vendorUrl("https://clickhouse.com/docs/integrations/java")
+        .excludes(List.of("*:*"))
+        .build();
+  }
+
   @Override
   public boolean isSupportsCustomDeleteStmt() {
     return true;
diff --git a/plugins/databases/cratedb/src/main/java/org/apache/hop/databases/cratedb/CrateDBDatabaseMeta.java b/plugins/databases/cratedb/src/main/java/org/apache/hop/databases/cratedb/CrateDBDatabaseMeta.java
index 3ecdf449c5..7acfd8b5ea 100644
--- a/plugins/databases/cratedb/src/main/java/org/apache/hop/databases/cratedb/CrateDBDatabaseMeta.java
+++ b/plugins/databases/cratedb/src/main/java/org/apache/hop/databases/cratedb/CrateDBDatabaseMeta.java
@@ -17,7 +17,9 @@
 
 package org.apache.hop.databases.cratedb;
 
+import java.util.List;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.databases.postgresql.PostgreSqlDatabaseMeta;
@@ -38,6 +40,20 @@ public class CrateDBDatabaseMeta extends PostgreSqlDatabaseMeta {
     return "io.crate.client.jdbc.CrateDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("io.crate:crate-jdbc")
+        .defaultVersion("2.7.0")
+        .licenseCategory("A")
+        .licenseName("Apache-2.0")
+        .licenseUrl("https://github.com/crate/crate-jdbc/blob/master/LICENSE")
+        .vendor("CrateDB")
+        .vendorUrl("https://cratedb.com/docs/jdbc/")
+        .excludes(List.of("com.fasterxml.jackson.core:*", "com.google.guava:*", "org.slf4j:*"))
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     return "jdbc:crate://" + hostname + ":" + port + "/" + databaseName;
diff --git a/plugins/databases/db2/src/main/java/org/apache/hop/databases/db2/DB2DatabaseMeta.java b/plugins/databases/db2/src/main/java/org/apache/hop/databases/db2/DB2DatabaseMeta.java
index 6d25423bfb..b379b13631 100644
--- a/plugins/databases/db2/src/main/java/org/apache/hop/databases/db2/DB2DatabaseMeta.java
+++ b/plugins/databases/db2/src/main/java/org/apache/hop/databases/db2/DB2DatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -59,6 +60,19 @@ public class DB2DatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "com.ibm.db2.jcc.DB2Driver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.ibm.db2:jcc")
+        .defaultVersion("12.1.5.0")
+        .licenseCategory("X")
+        .licenseName("IBM International Program License Agreement")
+        .licenseUrl("https://www.ibm.com/support/pages/db2-jdbc-driver-versions-and-downloads")
+        .vendor("IBM")
+        .vendorUrl("https://www.ibm.com/support/pages/db2-jdbc-driver-versions-and-downloads")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     return "jdbc:db2://" + hostname + ":" + port + "/" + databaseName;
diff --git a/plugins/databases/derby/src/main/java/org/apache/hop/databases/derby/DerbyDatabaseMeta.java b/plugins/databases/derby/src/main/java/org/apache/hop/databases/derby/DerbyDatabaseMeta.java
index cee958b76a..29aa0f02db 100644
--- a/plugins/databases/derby/src/main/java/org/apache/hop/databases/derby/DerbyDatabaseMeta.java
+++ b/plugins/databases/derby/src/main/java/org/apache/hop/databases/derby/DerbyDatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -58,6 +59,19 @@ public class DerbyDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     }
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("org.apache.derby:derbyclient")
+        .defaultVersion("10.17.1.0")
+        .licenseCategory("A")
+        .licenseName("Apache-2.0")
+        .licenseUrl("https://www.apache.org/licenses/LICENSE-2.0")
+        .vendor("Apache Derby")
+        .vendorUrl("https://db.apache.org/derby/")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     if (!Utils.isEmpty(hostname)) {
diff --git a/plugins/databases/duckdb/pom.xml b/plugins/databases/duckdb/pom.xml
index 53994b7b27..1f31c0ac78 100644
--- a/plugins/databases/duckdb/pom.xml
+++ b/plugins/databases/duckdb/pom.xml
@@ -27,29 +27,4 @@
     <artifactId>hop-databases-duckdb</artifactId>
     <packaging>jar</packaging>
     <name>Hop Plugins Databases DuckDB</name>
-
-    <properties>
-        <hop.jdbc.driver1>org.duckdb:duckdb_jdbc</hop.jdbc.driver1>
-    </properties>
-
-    <dependencyManagement>
-        <dependencies>
-            <dependency>
-                <groupId>org.apache.hop</groupId>
-                <artifactId>hop-libs-jdbc</artifactId>
-                <version>${project.version}</version>
-                <type>pom</type>
-                <scope>import</scope>
-            </dependency>
-        </dependencies>
-    </dependencyManagement>
-
-    <dependencies>
-        <dependency>
-            <groupId>org.duckdb</groupId>
-            <artifactId>duckdb_jdbc</artifactId>
-            <scope>provided</scope>
-        </dependency>
-    </dependencies>
-
 </project>
diff --git a/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java b/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java
index 56b2455aa7..ed41946c24 100644
--- a/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java
+++ b/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.exception.HopDatabaseException;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
@@ -140,6 +141,19 @@ public class DuckDBDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "org.duckdb.DuckDBDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("org.duckdb:duckdb_jdbc")
+        .defaultVersion("1.5.4.0")
+        .licenseCategory("A")
+        .licenseName("MIT")
+        .licenseUrl("https://github.com/duckdb/duckdb-java/blob/main/LICENSE")
+        .vendor("DuckDB")
+        .vendorUrl("https://duckdb.org/docs/stable/clients/java")
+        .build();
+  }
+
   @Override
   public boolean isDuckDbVariant() {
     return true;
diff --git a/plugins/databases/exasol4/src/main/java/org/apache/hop/databases/exasol4/Exasol4DatabaseMeta.java b/plugins/databases/exasol4/src/main/java/org/apache/hop/databases/exasol4/Exasol4DatabaseMeta.java
index 74a5d86aa9..812e4ca1c6 100644
--- a/plugins/databases/exasol4/src/main/java/org/apache/hop/databases/exasol4/Exasol4DatabaseMeta.java
+++ b/plugins/databases/exasol4/src/main/java/org/apache/hop/databases/exasol4/Exasol4DatabaseMeta.java
@@ -22,6 +22,7 @@ import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.Database;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.exception.HopDatabaseException;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
@@ -99,6 +100,19 @@ public class Exasol4DatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "com.exasol.jdbc.EXADriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.exasol:exasol-jdbc")
+        .defaultVersion("26.2.7")
+        .licenseCategory("X")
+        .licenseName("EXASOL License")
+        .licenseUrl("https://docs.exasol.com/db/latest/connect_exasol/drivers/jdbc.htm")
+        .vendor("Exasol")
+        .vendorUrl("https://www.exasol.com/")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName)
       throws HopDatabaseException {
diff --git a/plugins/databases/firebird/src/main/java/org/apache/hop/databases/firebird/FirebirdDatabaseMeta.java b/plugins/databases/firebird/src/main/java/org/apache/hop/databases/firebird/FirebirdDatabaseMeta.java
index 74281e0815..6a48ecdf93 100644
--- a/plugins/databases/firebird/src/main/java/org/apache/hop/databases/firebird/FirebirdDatabaseMeta.java
+++ b/plugins/databases/firebird/src/main/java/org/apache/hop/databases/firebird/FirebirdDatabaseMeta.java
@@ -17,10 +17,12 @@
 
 package org.apache.hop.databases.firebird;
 
+import java.util.List;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -67,6 +69,20 @@ public class FirebirdDatabaseMeta extends BaseDatabaseMeta implements IDatabase
     return "org.firebirdsql.jdbc.FBDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("org.firebirdsql.jdbc:jaybird")
+        .defaultVersion("6.0.5")
+        .licenseCategory("X")
+        .licenseName("LGPL-2.1")
+        .licenseUrl("https://github.com/FirebirdSQL/jaybird/blob/master/LICENSE")
+        .vendor("Firebird (Jaybird)")
+        .vendorUrl("https://firebirdsql.org/en/jdbc-driver/")
+        .excludes(List.of("org.jspecify:jspecify"))
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     return "jdbc:firebirdsql://" + hostname + ":" + port + "/" + databaseName;
diff --git a/plugins/databases/h2/src/main/java/org/apache/hop/databases/h2/H2DatabaseMeta.java b/plugins/databases/h2/src/main/java/org/apache/hop/databases/h2/H2DatabaseMeta.java
index c8a63fea11..e40bb4764b 100644
--- a/plugins/databases/h2/src/main/java/org/apache/hop/databases/h2/H2DatabaseMeta.java
+++ b/plugins/databases/h2/src/main/java/org/apache/hop/databases/h2/H2DatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -51,6 +52,19 @@ public class H2DatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "org.h2.Driver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.h2database:h2")
+        .defaultVersion("2.4.240")
+        .licenseCategory("B")
+        .licenseName("EPL-1.0 / MPL-2.0")
+        .licenseUrl("https://www.h2database.com/html/license.html")
+        .vendor("H2 Database Engine")
+        .vendorUrl("https://www.h2database.com/")
+        .build();
+  }
+
   @Override
   public int getDefaultDatabasePort() {
     if (getAccessType() == DatabaseMeta.TYPE_ACCESS_NATIVE) {
diff --git a/plugins/databases/hive/src/main/java/org/apache/hop/databases/hive/HiveDatabaseMeta.java b/plugins/databases/hive/src/main/java/org/apache/hop/databases/hive/HiveDatabaseMeta.java
index 36c1db91f2..78b601f512 100644
--- a/plugins/databases/hive/src/main/java/org/apache/hop/databases/hive/HiveDatabaseMeta.java
+++ b/plugins/databases/hive/src/main/java/org/apache/hop/databases/hive/HiveDatabaseMeta.java
@@ -24,6 +24,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiElementType;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
@@ -137,6 +138,20 @@ public class HiveDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "org.apache.hive.jdbc.HiveDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("org.apache.hive:hive-jdbc:jar:standalone")
+        .defaultVersion("4.2.0")
+        .licenseCategory("A")
+        .licenseName("Apache-2.0")
+        .licenseUrl("https://www.apache.org/licenses/LICENSE-2.0")
+        .vendor("Apache Hive")
+        .vendorUrl("https://hive.apache.org/")
+        .excludes(List.of("*:*"))
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     // Split the hostnames and ports up using commas.
diff --git a/plugins/databases/hypersonic/src/main/java/org/apache/hop/databases/hypersonic/HypersonicDatabaseMeta.java b/plugins/databases/hypersonic/src/main/java/org/apache/hop/databases/hypersonic/HypersonicDatabaseMeta.java
index 64f3ad7e72..33e338eaeb 100644
--- a/plugins/databases/hypersonic/src/main/java/org/apache/hop/databases/hypersonic/HypersonicDatabaseMeta.java
+++ b/plugins/databases/hypersonic/src/main/java/org/apache/hop/databases/hypersonic/HypersonicDatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -51,6 +52,19 @@ public class HypersonicDatabaseMeta extends BaseDatabaseMeta implements IDatabas
     return "org.hsqldb.jdbcDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("org.hsqldb:hsqldb")
+        .defaultVersion("2.7.4")
+        .licenseCategory("A")
+        .licenseName("BSD-3-Clause (HSQLDB License)")
+        .licenseUrl("https://hsqldb.org/web/hsqlLicense.html")
+        .vendor("HSQLDB")
+        .vendorUrl("https://hsqldb.org/")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     if ((Utils.isEmpty(port) || "-1".equals(port)) && Utils.isEmpty(hostname)) {
diff --git a/plugins/databases/informix/src/main/java/org/apache/hop/databases/informix/InformixDatabaseMeta.java b/plugins/databases/informix/src/main/java/org/apache/hop/databases/informix/InformixDatabaseMeta.java
index 43926d6e45..836abbb0b7 100644
--- a/plugins/databases/informix/src/main/java/org/apache/hop/databases/informix/InformixDatabaseMeta.java
+++ b/plugins/databases/informix/src/main/java/org/apache/hop/databases/informix/InformixDatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiElementType;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
@@ -73,6 +74,19 @@ public class InformixDatabaseMeta extends BaseDatabaseMeta implements IDatabase
     return "com.informix.jdbc.IfxDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.ibm.informix:jdbc")
+        .defaultVersion("15.0.1.2")
+        .licenseCategory("X")
+        .licenseName("IBM International Program License Agreement")
+        .licenseUrl("https://www.ibm.com/products/informix")
+        .vendor("IBM Informix")
+        .vendorUrl("https://www.ibm.com/docs/en/informix-servers")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     return "jdbc:informix-sqli://"
diff --git a/plugins/databases/ingres/src/main/java/org/apache/hop/databases/ingres/IngresDatabaseMeta.java b/plugins/databases/ingres/src/main/java/org/apache/hop/databases/ingres/IngresDatabaseMeta.java
index ce2474d625..0319c1c599 100644
--- a/plugins/databases/ingres/src/main/java/org/apache/hop/databases/ingres/IngresDatabaseMeta.java
+++ b/plugins/databases/ingres/src/main/java/org/apache/hop/databases/ingres/IngresDatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -54,6 +55,19 @@ public class IngresDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "com.ingres.jdbc.IngresDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.ingres.jdbc:iijdbc")
+        .defaultVersion("12.1-4.6.5")
+        .licenseCategory("X")
+        .licenseName("Actian Ingres JDBC License")
+        .licenseUrl("https://www.actian.com/")
+        .vendor("Actian Ingres")
+        .vendorUrl("https://docs.actian.com/")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     if (Utils.isEmpty(port)) {
diff --git a/plugins/databases/mariadb/src/main/java/org/apache/hop/databases/mariadb/MariaDBDatabaseMeta.java b/plugins/databases/mariadb/src/main/java/org/apache/hop/databases/mariadb/MariaDBDatabaseMeta.java
index 86dab1da4b..c388246d7f 100644
--- a/plugins/databases/mariadb/src/main/java/org/apache/hop/databases/mariadb/MariaDBDatabaseMeta.java
+++ b/plugins/databases/mariadb/src/main/java/org/apache/hop/databases/mariadb/MariaDBDatabaseMeta.java
@@ -21,6 +21,7 @@ import java.sql.DatabaseMetaData;
 import java.sql.ResultSetMetaData;
 import java.util.Set;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.exception.HopDatabaseException;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.util.Utils;
@@ -44,6 +45,20 @@ public class MariaDBDatabaseMeta extends MySqlDatabaseMeta {
     return "org.mariadb.jdbc.Driver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("org.mariadb.jdbc:mariadb-java-client")
+        .defaultVersion("3.5.9")
+        .licenseCategory("X")
+        .licenseName("LGPL-2.1")
+        .licenseUrl(
+            "https://github.com/mariadb-corporation/mariadb-connector-j/blob/master/LICENSE")
+        .vendor("MariaDB")
+        .vendorUrl("https://mariadb.com/kb/en/about-mariadb-connector-j/")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     if (Utils.isEmpty(port)) {
diff --git a/plugins/databases/monetdb/src/main/java/org/apache/hop/databases/monetdb/MonetDBDatabaseMeta.java b/plugins/databases/monetdb/src/main/java/org/apache/hop/databases/monetdb/MonetDBDatabaseMeta.java
index 4e9fea430e..ec9bb013b2 100644
--- a/plugins/databases/monetdb/src/main/java/org/apache/hop/databases/monetdb/MonetDBDatabaseMeta.java
+++ b/plugins/databases/monetdb/src/main/java/org/apache/hop/databases/monetdb/MonetDBDatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -78,6 +79,20 @@ public class MonetDBDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "org.monetdb.jdbc.MonetDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("monetdb:monetdb-jdbc")
+        .defaultVersion("12.2")
+        .licenseCategory("B")
+        .licenseName("MPL-2.0")
+        .licenseUrl("https://www.mozilla.org/en-US/MPL/2.0/")
+        .vendor("MonetDB")
+        .vendorUrl("https://www.monetdb.org/")
+        .repositoryUrl("https://clojars.org/repo/")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     if (Utils.isEmpty(port)) {
diff --git a/plugins/databases/mssql/src/main/java/org/apache/hop/databases/mssql/MsSqlServerDatabaseMeta.java b/plugins/databases/mssql/src/main/java/org/apache/hop/databases/mssql/MsSqlServerDatabaseMeta.java
index b3767bd415..8e92a0fd1e 100644
--- a/plugins/databases/mssql/src/main/java/org/apache/hop/databases/mssql/MsSqlServerDatabaseMeta.java
+++ b/plugins/databases/mssql/src/main/java/org/apache/hop/databases/mssql/MsSqlServerDatabaseMeta.java
@@ -23,6 +23,7 @@ import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.Database;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.exception.HopDatabaseException;
 import org.apache.hop.core.gui.plugin.GuiElementType;
@@ -108,6 +109,19 @@ public class MsSqlServerDatabaseMeta extends BaseDatabaseMeta implements IDataba
     return "net.sourceforge.jtds.jdbc.Driver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("net.sourceforge.jtds:jtds")
+        .defaultVersion("1.3.1")
+        .licenseCategory("X")
+        .licenseName("LGPL-2.1")
+        .licenseUrl("https://jtds.sourceforge.net/license.html")
+        .vendor("jTDS")
+        .vendorUrl("https://jtds.sourceforge.net/")
+        .build();
+  }
+
   /**
    * The URL format for jTDS is:
    *
diff --git a/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java b/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
index 545f514529..68efe29d62 100644
--- a/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
+++ b/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
@@ -23,6 +23,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.gui.plugin.GuiElementType;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.gui.plugin.GuiWidgetElement;
@@ -120,6 +121,20 @@ public class MsSqlServerNativeDatabaseMeta extends MsSqlServerDatabaseMeta
     return "com.microsoft.sqlserver.jdbc.SQLServerDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.microsoft.sqlserver:mssql-jdbc")
+        .defaultVersion("13.4.0.jre11")
+        .licenseCategory("A")
+        .licenseName("MIT")
+        .licenseUrl("https://github.com/microsoft/mssql-jdbc/blob/main/LICENSE")
+        .vendor("Microsoft")
+        .vendorUrl(
+            "https://learn.microsoft.com/sql/connect/jdbc/download-microsoft-jdbc-driver-for-sql-server")
+        .build();
+  }
+
   @Override
   public String getURL(String serverName, String port, String databaseName) {
 
diff --git a/plugins/databases/mysql/src/main/java/org/apache/hop/databases/mysql/MySqlDatabaseMeta.java b/plugins/databases/mysql/src/main/java/org/apache/hop/databases/mysql/MySqlDatabaseMeta.java
index 490f31d5e7..1f10f41a65 100644
--- a/plugins/databases/mysql/src/main/java/org/apache/hop/databases/mysql/MySqlDatabaseMeta.java
+++ b/plugins/databases/mysql/src/main/java/org/apache/hop/databases/mysql/MySqlDatabaseMeta.java
@@ -27,6 +27,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.exception.HopDatabaseException;
 import org.apache.hop.core.gui.plugin.GuiElementType;
@@ -157,12 +158,23 @@ public class MySqlDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     };
   }
 
-  public String getDriverClassName() {
-    return driverClassName;
-  }
-
-  public void setDriverClassName(String driverClassName) {
-    this.driverClassName = driverClassName;
+  @Override
+  public DriverDownload getDriverDownload() {
+    // Only offer MySQL Connector/J for databases that actually use it (Doris, Infobright, ...).
+    String driverClass = getDriverClass();
+    if (driverClass == null || !driverClass.contains("mysql")) {
+      return null;
+    }
+    return DriverDownload.builder()
+        .mavenCoordinate("com.mysql:mysql-connector-j")
+        .defaultVersion("9.7.0")
+        .licenseCategory("X")
+        .licenseName("GPLv2 with Universal FOSS Exception")
+        .licenseUrl("https://github.com/mysql/mysql-connector-j/blob/release/9.x/LICENSE")
+        .vendor("Oracle / MySQL")
+        .vendorUrl("https://dev.mysql.com/downloads/connector/j/")
+        .excludes(List.of("com.google.protobuf:protobuf-java"))
+        .build();
   }
 
   @Override
diff --git a/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleDatabaseMeta.java b/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleDatabaseMeta.java
index 893ffe4122..1cd78a5d9c 100644
--- a/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleDatabaseMeta.java
+++ b/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleDatabaseMeta.java
@@ -23,6 +23,7 @@ import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.Database;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.database.SqlScriptParser;
 import org.apache.hop.core.exception.HopDatabaseException;
@@ -104,6 +105,19 @@ public class OracleDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "oracle.jdbc.driver.OracleDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.oracle.database.jdbc:ojdbc11")
+        .defaultVersion("23.26.2.0.0")
+        .licenseCategory("X")
+        .licenseName("Oracle Free Use Terms and Conditions (OTN)")
+        .licenseUrl("https://www.oracle.com/downloads/licenses/oracle-free-license.html")
+        .vendor("Oracle")
+        .vendorUrl("https://www.oracle.com/database/technologies/appdev/jdbc.html")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName)
       throws HopDatabaseException {
diff --git a/plugins/databases/postgresql/src/main/java/org/apache/hop/databases/postgresql/PostgreSqlDatabaseMeta.java b/plugins/databases/postgresql/src/main/java/org/apache/hop/databases/postgresql/PostgreSqlDatabaseMeta.java
index b4925f4b0c..baf132ba94 100644
--- a/plugins/databases/postgresql/src/main/java/org/apache/hop/databases/postgresql/PostgreSqlDatabaseMeta.java
+++ b/plugins/databases/postgresql/src/main/java/org/apache/hop/databases/postgresql/PostgreSqlDatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -76,6 +77,25 @@ public class PostgreSqlDatabaseMeta extends BaseDatabaseMeta implements IDatabas
     return "org.postgresql.Driver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    // Only offer the PostgreSQL driver for databases that actually use it. Subclasses that use a
+    // different JDBC driver (CrateDB, Redshift, ...) override getDriverClass() and declare their
+    // own.
+    if (!"org.postgresql.Driver".equals(getDriverClass())) {
+      return null;
+    }
+    return DriverDownload.builder()
+        .mavenCoordinate("org.postgresql:postgresql")
+        .defaultVersion("42.7.11")
+        .licenseCategory("A")
+        .licenseName("BSD-2-Clause")
+        .licenseUrl("https://jdbc.postgresql.org/about/license/")
+        .vendor("PostgreSQL Global Development Group")
+        .vendorUrl("https://jdbc.postgresql.org/")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     return "jdbc:postgresql://" + hostname + ":" + port + "/" + databaseName;
diff --git a/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftDatabaseMeta.java b/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftDatabaseMeta.java
index 33cfd150c8..f974b8f11b 100644
--- a/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftDatabaseMeta.java
+++ b/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftDatabaseMeta.java
@@ -18,6 +18,7 @@ package org.apache.hop.databases.redshift;
 
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.databases.postgresql.PostgreSqlDatabaseMeta;
 
@@ -46,6 +47,19 @@ public class RedshiftDatabaseMeta extends PostgreSqlDatabaseMeta {
     return "com.amazon.redshift.jdbc42.Driver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.amazon.redshift:redshift-jdbc42")
+        .defaultVersion("2.2.7")
+        .licenseCategory("A")
+        .licenseName("Apache-2.0")
+        .licenseUrl("https://github.com/aws/amazon-redshift-jdbc-driver/blob/master/LICENSE")
+        .vendor("Amazon Web Services")
+        .vendorUrl("https://docs.aws.amazon.com/redshift/latest/mgmt/jdbc20-download-driver.html")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     return "jdbc:redshift://" + hostname + ":" + port + "/" + databaseName;
diff --git a/plugins/databases/singlestore/src/main/java/org/apache/hop/databases/singlestore/SingleStoreDatabaseMeta.java b/plugins/databases/singlestore/src/main/java/org/apache/hop/databases/singlestore/SingleStoreDatabaseMeta.java
index b1da87ee03..0566317450 100644
--- a/plugins/databases/singlestore/src/main/java/org/apache/hop/databases/singlestore/SingleStoreDatabaseMeta.java
+++ b/plugins/databases/singlestore/src/main/java/org/apache/hop/databases/singlestore/SingleStoreDatabaseMeta.java
@@ -27,6 +27,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.exception.HopDatabaseException;
 import org.apache.hop.core.gui.plugin.GuiElementType;
@@ -91,6 +92,19 @@ public class SingleStoreDatabaseMeta extends BaseDatabaseMeta implements IDataba
     return "com.singlestore.jdbc.Driver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.singlestore:singlestore-jdbc-client")
+        .defaultVersion("1.2.11")
+        .licenseCategory("X")
+        .licenseName("LGPL-2.1")
+        .licenseUrl("https://github.com/memsql/S2-JDBC-Connector/blob/main/LICENSE")
+        .vendor("SingleStore")
+        .vendorUrl("https://docs.singlestore.com/")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     String url = "jdbc:singlestore:";
diff --git a/plugins/databases/snowflake/pom.xml b/plugins/databases/snowflake/pom.xml
index 7c1598f1a3..1d10ef2136 100644
--- a/plugins/databases/snowflake/pom.xml
+++ b/plugins/databases/snowflake/pom.xml
@@ -27,29 +27,4 @@
     <artifactId>hop-databases-snowflake</artifactId>
     <packaging>jar</packaging>
     <name>Hop Plugins Databases Snowflake</name>
-
-    <properties>
-        <hop.jdbc.driver1>net.snowflake:snowflake-jdbc</hop.jdbc.driver1>
-    </properties>
-
-    <dependencyManagement>
-        <dependencies>
-            <dependency>
-                <groupId>org.apache.hop</groupId>
-                <artifactId>hop-libs-jdbc</artifactId>
-                <version>${project.version}</version>
-                <type>pom</type>
-                <scope>import</scope>
-            </dependency>
-        </dependencies>
-    </dependencyManagement>
-
-    <dependencies>
-        <dependency>
-            <groupId>net.snowflake</groupId>
-            <artifactId>snowflake-jdbc</artifactId>
-            <scope>provided</scope>
-        </dependency>
-    </dependencies>
-
 </project>
diff --git a/plugins/databases/snowflake/src/main/java/org/apache/hop/databases/snowflake/SnowflakeDatabaseMeta.java b/plugins/databases/snowflake/src/main/java/org/apache/hop/databases/snowflake/SnowflakeDatabaseMeta.java
index ca95283650..a27172d4ca 100644
--- a/plugins/databases/snowflake/src/main/java/org/apache/hop/databases/snowflake/SnowflakeDatabaseMeta.java
+++ b/plugins/databases/snowflake/src/main/java/org/apache/hop/databases/snowflake/SnowflakeDatabaseMeta.java
@@ -25,6 +25,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiElementType;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
@@ -98,6 +99,19 @@ public class SnowflakeDatabaseMeta extends BaseDatabaseMeta implements IDatabase
     return "net.snowflake.client.jdbc.SnowflakeDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("net.snowflake:snowflake-jdbc")
+        .defaultVersion("4.3.1")
+        .licenseCategory("A")
+        .licenseName("Apache-2.0")
+        .licenseUrl("https://github.com/snowflakedb/snowflake-jdbc/blob/master/LICENSE.txt")
+        .vendor("Snowflake")
+        .vendorUrl("https://docs.snowflake.com/en/developer-guide/jdbc/jdbc")
+        .build();
+  }
+
   @Override
   public String getURL(String hostName, String port, String databaseName) {
 
diff --git a/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java b/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java
index a8459120ba..9f9cfc21c6 100644
--- a/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java
+++ b/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -54,6 +55,19 @@ public class SqliteDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "org.sqlite.JDBC";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("org.xerial:sqlite-jdbc")
+        .defaultVersion("3.53.2.0")
+        .licenseCategory("A")
+        .licenseName("Apache-2.0")
+        .licenseUrl("https://github.com/xerial/sqlite-jdbc/blob/master/LICENSE")
+        .vendor("Xerial SQLite JDBC")
+        .vendorUrl("https://github.com/xerial/sqlite-jdbc")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     return "jdbc:sqlite:" + databaseName;
diff --git a/plugins/databases/sybase/src/main/java/org/apache/hop/databases/sybase/SybaseDatabaseMeta.java b/plugins/databases/sybase/src/main/java/org/apache/hop/databases/sybase/SybaseDatabaseMeta.java
index 647627d1c2..f61f01caf6 100644
--- a/plugins/databases/sybase/src/main/java/org/apache/hop/databases/sybase/SybaseDatabaseMeta.java
+++ b/plugins/databases/sybase/src/main/java/org/apache/hop/databases/sybase/SybaseDatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -61,6 +62,19 @@ public class SybaseDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "net.sourceforge.jtds.jdbc.Driver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("net.sourceforge.jtds:jtds")
+        .defaultVersion("1.3.1")
+        .licenseCategory("X")
+        .licenseName("LGPL-2.1")
+        .licenseUrl("https://jtds.sourceforge.net/license.html")
+        .vendor("jTDS")
+        .vendorUrl("https://jtds.sourceforge.net/")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     // jdbc:jtds:<server_type>://<server>[:<port>][/<database>][;<property>=<value>[;...]]
diff --git a/plugins/databases/teradata/src/main/java/org/apache/hop/databases/teradata/TeradataDatabaseMeta.java b/plugins/databases/teradata/src/main/java/org/apache/hop/databases/teradata/TeradataDatabaseMeta.java
index aaac457673..11f3fe3ea3 100644
--- a/plugins/databases/teradata/src/main/java/org/apache/hop/databases/teradata/TeradataDatabaseMeta.java
+++ b/plugins/databases/teradata/src/main/java/org/apache/hop/databases/teradata/TeradataDatabaseMeta.java
@@ -23,6 +23,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -65,6 +66,19 @@ public class TeradataDatabaseMeta extends BaseDatabaseMeta implements IDatabase
     return "com.teradata.jdbc.TeraDriver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.teradata.jdbc:terajdbc")
+        .defaultVersion("20.00.00.58")
+        .licenseCategory("X")
+        .licenseName("Teradata JDBC Driver License Agreement")
+        .licenseUrl("https://downloads.teradata.com/download/connectivity/jdbc-driver")
+        .vendor("Teradata")
+        .vendorUrl("https://downloads.teradata.com/download/connectivity/jdbc-driver")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     String url = "jdbc:teradata://" + hostname;
diff --git a/plugins/databases/vertica/src/main/java/org/apache/hop/databases/vertica/VerticaDatabaseMeta.java b/plugins/databases/vertica/src/main/java/org/apache/hop/databases/vertica/VerticaDatabaseMeta.java
index 99fccd27e1..e07fa99303 100644
--- a/plugins/databases/vertica/src/main/java/org/apache/hop/databases/vertica/VerticaDatabaseMeta.java
+++ b/plugins/databases/vertica/src/main/java/org/apache/hop/databases/vertica/VerticaDatabaseMeta.java
@@ -21,6 +21,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
@@ -50,6 +51,20 @@ public class VerticaDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
     return "com.vertica.Driver";
   }
 
+  @Override
+  public DriverDownload getDriverDownload() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.vertica.jdbc:vertica-jdbc")
+        .defaultVersion("25.3.0-0")
+        .licenseCategory("X")
+        .licenseName("Vertica JDBC Driver License")
+        .licenseUrl(
+            "https://docs.vertica.com/latest/en/connecting-to/client-libraries/client-drivers/jdbc/")
+        .vendor("OpenText Vertica")
+        .vendorUrl("https://docs.vertica.com/")
+        .build();
+  }
+
   @Override
   public String getURL(String hostname, String port, String databaseName) {
     return "jdbc:vertica://" + hostname + ":" + port + "/" + databaseName;
diff --git a/pom.xml b/pom.xml
index a430d70e34..89d800c5a1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -451,6 +451,7 @@
                         </goals>
                         <configuration>
                             <argLine>@{argLine} -Dfile.encoding=${project.build.sourceEncoding}
+                                -DHOP_AUDIT_FOLDER=${project.build.directory}/audit
                                 ${maven-surefire-plugin.argLine} ${ui.test.argLine}</argLine>
                             <forkCount>${maven-surefire-plugin.forkCount}</forkCount>
                             <reuseForks>${maven-surefire-plugin.reuseForks}</reuseForks>
diff --git a/rcp/src/test/java/org/apache/hop/ui/core/database/JdbcDriverDownloadDialogTest.java b/rcp/src/test/java/org/apache/hop/ui/core/database/JdbcDriverDownloadDialogTest.java
new file mode 100644
index 0000000000..3b75ef6cc9
--- /dev/null
+++ b/rcp/src/test/java/org/apache/hop/ui/core/database/JdbcDriverDownloadDialogTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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
+ *
+ *       http://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.hop.ui.core.database;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.hop.core.database.DriverDownload;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.testing.SwtBotTestBase;
+import org.eclipse.swtbot.swt.finder.SWTBot;
+import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException;
+import org.eclipse.swtbot.swt.finder.widgets.SWTBotButton;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * SWTBot coverage for {@link JdbcDriverDownloadDialog}'s license gate. The dialog lives in the
+ * {@code hop-ui} module but its SWTBot test runs here in {@code rcp}: opening it warms up {@link
+ * org.apache.hop.ui.core.PropsUi}, whose {@code TextSizeUtilFacade} needs the {@code rcp}
+ * implementation that {@code hop-ui} cannot depend on itself.
+ *
+ * <p>The dialog runs its own blocking event loop in {@code open()}, so {@link
+ * SwtBotTestBase#withDialog} pumps it on the UI thread while the assertions drive it from a worker
+ * thread. Both tests Cancel before any download, so nothing is fetched over the network.
+ *
+ * <p>Tagged {@code uitest} so it is excluded from the normal headless build (it needs a display);
+ * run with {@code mvn -pl rcp -Puitest test}.
+ */
+@Tag("uitest")
+class JdbcDriverDownloadDialogTest extends SwtBotTestBase {
+
+  private static final Class<?> PKG = JdbcDriverDownloadDialog.class;
+
+  /**
+   * A restricted (Category X) driver: Download must stay disabled until the license is accepted.
+   */
+  private static DriverDownload restrictedDriver() {
+    return DriverDownload.builder()
+        .mavenCoordinate("com.example:demo-jdbc")
+        .defaultVersion("1.0.0")
+        .licenseCategory("X")
+        .licenseName("Demo Proprietary License")
+        .licenseUrl("https://example.com/license")
+        .vendor("Example")
+        .vendorUrl("https://example.com")
+        .build();
+  }
+
+  /** An open (Category A) driver: Download is available immediately, no acceptance checkbox. */
+  private static DriverDownload openDriver() {
+    return DriverDownload.builder()
+        .mavenCoordinate("org.example:open-jdbc")
+        .defaultVersion("2.0.0")
+        .licenseCategory("A")
+        .licenseName("Apache-2.0")
+        .licenseUrl("https://www.apache.org/licenses/LICENSE-2.0")
+        .vendor("Example")
+        .vendorUrl("https://example.com")
+        .build();
+  }
+
+  @Test
+  void restrictedDriverGatesDownloadOnLicenseAcceptance() {
+    AtomicBoolean installed = new AtomicBoolean(true);
+
+    withDialog(
+        parent ->
+            installed.set(
+                new JdbcDriverDownloadDialog(parent, "DEMO", "Demo DB", restrictedDriver()).open()),
+        bot -> {
+          SWTBot dialog = bot.shell(shellTitle()).activate().bot();
+
+          SWTBotButton download = dialog.button(downloadLabel());
+          assertFalse(
+              download.isEnabled(), "Download must be disabled until the license is accepted");
+
+          dialog.checkBox().click(); // accept the vendor license
+          assertTrue(download.isEnabled(), "accepting the license must enable Download");
+
+          dialog.button(buttonLabel("System.Button.Cancel")).click(); // no network download
+        });
+
+    assertFalse(installed.get(), "Cancel must report the driver as not installed");
+  }
+
+  @Test
+  void openDriverEnablesDownloadWithoutALicenseCheckbox() {
+    AtomicBoolean installed = new AtomicBoolean(true);
+
+    withDialog(
+        parent ->
+            installed.set(
+                new JdbcDriverDownloadDialog(parent, "OPEN", "Open DB", openDriver()).open()),
+        bot -> {
+          SWTBot dialog = bot.shell(shellTitle()).activate().bot();
+
+          assertTrue(
+              dialog.button(downloadLabel()).isEnabled(),
+              "an open-license driver must be downloadable without accepting anything");
+          assertThrows(
+              WidgetNotFoundException.class,
+              dialog::checkBox,
+              "an open-license driver must not show a license-acceptance checkbox");
+
+          dialog.button(buttonLabel("System.Button.Cancel")).click();
+        });
+
+    assertFalse(installed.get(), "Cancel must report the driver as not installed");
+  }
+
+  private static String shellTitle() {
+    return BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Shell.Title");
+  }
+
+  private static String downloadLabel() {
+    return BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Button.Download");
+  }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java b/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java
index 020819cd2b..865d9cf17f 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java
@@ -32,6 +32,7 @@ import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabasePluginType;
 import org.apache.hop.core.database.DatabaseTestResults;
+import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.plugins.IPlugin;
@@ -88,6 +89,7 @@ public class DatabaseMetaEditor extends MetadataEditor<DatabaseMeta> {
   private Text wName;
   private Combo wConnectionType;
   private Label wDriverInfo;
+  private Button wbDownloadDriver;
   private TextVar wManualUrl;
   private Label wlUsername;
   private TextVar wUsername;
@@ -340,15 +342,27 @@ public class DatabaseMetaEditor extends MetadataEditor<DatabaseMeta> {
     fdlDriverInfo.right = new FormAttachment(middle, -margin);
     wlDriverInfo.setLayoutData(fdlDriverInfo);
 
+    // "Download driver" button - shown only when the selected database type has a downloadable
+    // driver in the catalog that isn't installed yet (toggled in updateDriverInfo()).
+    wbDownloadDriver = new Button(wGeneralComp, SWT.PUSH);
+    PropsUi.setLook(wbDownloadDriver);
+    wbDownloadDriver.setText(BaseMessages.getString(PKG, "DatabaseDialog.button.DownloadDriver"));
+    FormData fdDownloadDriver = new FormData();
+    fdDownloadDriver.top = new FormAttachment(wlDriverInfo, 0, SWT.CENTER);
+    fdDownloadDriver.right = new FormAttachment(100, 0);
+    wbDownloadDriver.setLayoutData(fdDownloadDriver);
+    wbDownloadDriver.addListener(SWT.Selection, e -> downloadDriver());
+    wbDownloadDriver.setVisible(false);
+
     wDriverInfo = new Label(wGeneralComp, SWT.LEFT);
     wDriverInfo.setEnabled(false);
     PropsUi.setLook(wDriverInfo);
     FormData fdDriverInfo = new FormData();
     fdDriverInfo.top = new FormAttachment(wlDriverInfo, 0, SWT.CENTER);
     fdDriverInfo.left = new FormAttachment(middle, 0);
-    fdDriverInfo.right = new FormAttachment(100, 0);
+    fdDriverInfo.right = new FormAttachment(wbDownloadDriver, -margin);
     wDriverInfo.setLayoutData(fdDriverInfo);
-    lastControl = wDriverInfo;
+    lastControl = wbDownloadDriver;
 
     // Username field - only create if not excluded
     //
@@ -1194,22 +1208,66 @@ public class DatabaseMetaEditor extends MetadataEditor<DatabaseMeta> {
   protected void updateDriverInfo() {
     String driverName;
     String driverVersion = null;
+    boolean driverLoaded = false;
+    IDatabase database = null;
     try {
       DatabaseMeta databaseMeta = new DatabaseMeta();
       this.getWidgetsContent(databaseMeta);
+      database = databaseMeta.getIDatabase();
 
       driverName = databaseMeta.getDriverClass(getVariables());
       if (!Utils.isEmpty(driverName)) {
-        ClassLoader classLoader = databaseMeta.getIDatabase().getClass().getClassLoader();
+        ClassLoader classLoader = database.getClass().getClassLoader();
         Class<? extends Driver> driverClass =
             classLoader.loadClass(driverName).asSubclass(Driver.class);
         driverVersion = getDriverVersion(driverClass);
+        driverLoaded = true;
       }
     } catch (Exception e) {
       driverName = "No driver installed";
     }
 
     wDriverInfo.setText(driverName + (driverVersion != null ? " (" + driverVersion + ")" : ""));
+    updateDownloadButton(database, driverLoaded);
+  }
+
+  /**
+   * Show the "Download driver" button only when the selected database type declares a downloadable
+   * driver ({@link IDatabase#getDriverDownload()}) and that driver isn't loadable yet.
+   */
+  private void updateDownloadButton(IDatabase database, boolean driverLoaded) {
+    if (wbDownloadDriver == null || wbDownloadDriver.isDisposed()) {
+      return;
+    }
+    boolean show = !driverLoaded && database != null && database.getDriverDownload() != null;
+    wbDownloadDriver.setVisible(show);
+  }
+
+  /** Download + install the JDBC driver for the currently selected database type. */
+  private void downloadDriver() {
+    try {
+      DatabaseMeta databaseMeta = new DatabaseMeta();
+      this.getWidgetsContent(databaseMeta);
+      IDatabase database = databaseMeta.getIDatabase();
+      DriverDownload download = database == null ? null : database.getDriverDownload();
+      if (download == null) {
+        return;
+      }
+      JdbcDriverDownloadDialog dialog =
+          new JdbcDriverDownloadDialog(
+              getShell(), databaseMeta.getPluginId(), databaseMeta.getPluginName(), download);
+      if (dialog.open()) {
+        // The dialog hot-loads the driver into the plugin classloader, so refresh the label - it
+        // may already be available without a restart.
+        updateDriverInfo();
+      }
+    } catch (Exception e) {
+      new ErrorDialog(
+          getShell(),
+          BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Error.Title"),
+          BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Error.Generic"),
+          e);
+    }
   }
 
   /**
diff --git a/ui/src/main/java/org/apache/hop/ui/core/database/JdbcDriverDownloadDialog.java b/ui/src/main/java/org/apache/hop/ui/core/database/JdbcDriverDownloadDialog.java
new file mode 100644
index 0000000000..46d10fec6c
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/database/JdbcDriverDownloadDialog.java
@@ -0,0 +1,278 @@
+/*
+ * 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
+ *
+ *       http://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.hop.ui.core.database;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.IRunnableWithProgress;
+import org.apache.hop.core.database.DriverDownload;
+import org.apache.hop.driver.DriverInstaller;
+import org.apache.hop.driver.DriverResolver;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.core.dialog.ProgressMonitorDialog;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.program.Program;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * A resizable, movable dialog that confirms downloading a JDBC driver and - for restricted
+ * (Category X) drivers - has the user accept the vendor license, then downloads it from Maven
+ * Central into lib/jdbc and hot-loads it into the database plugin's classloader.
+ *
+ * <p>ASF compliance: Apache Hop never bundles, hosts or mirrors restricted drivers. This dialog
+ * surfaces the license and only fetches the driver, from Maven Central onto the user's machine,
+ * after the user explicitly accepts and clicks Download. The driver descriptor itself comes from
+ * the database plugin ({@link DriverDownload}).
+ */
+public class JdbcDriverDownloadDialog {
+  private static final Class<?> PKG = JdbcDriverDownloadDialog.class;
+
+  private final Shell parent;
+  private final String databaseType;
+  private final String driverName;
+  private final DriverDownload download;
+
+  private Shell shell;
+  private final PropsUi props;
+  private Button wAccept;
+  private Button wDownload;
+  private boolean installed;
+
+  public JdbcDriverDownloadDialog(
+      Shell parent, String databaseType, String driverName, DriverDownload download) {
+    this.parent = parent;
+    this.databaseType = databaseType;
+    this.driverName = driverName;
+    this.download = download;
+    this.props = PropsUi.getInstance();
+  }
+
+  /**
+   * @return true if the driver was downloaded and installed, false if the user cancelled or it
+   *     failed.
+   */
+  public boolean open() {
+    // A normal resizable, movable window (no SWT.SHEET, which on macOS attaches a fixed sheet).
+    shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.APPLICATION_MODAL);
+    PropsUi.setLook(shell);
+    shell.setImage(GuiResource.getInstance().getImageHopUi());
+    shell.setText(BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Shell.Title"));
+
+    FormLayout formLayout = new FormLayout();
+    formLayout.marginWidth = PropsUi.getFormMargin();
+    formLayout.marginHeight = PropsUi.getFormMargin();
+    shell.setLayout(formLayout);
+
+    int margin = PropsUi.getMargin();
+    boolean restricted = download.isRestricted();
+
+    // Intro line
+    Label wIntro = new Label(shell, SWT.WRAP);
+    PropsUi.setLook(wIntro);
+    wIntro.setText(
+        restricted
+            ? BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Intro.Restricted", driverName)
+            : BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Intro.Open", driverName));
+    FormData fdIntro = new FormData();
+    fdIntro.top = new FormAttachment(0, margin);
+    fdIntro.left = new FormAttachment(0, 0);
+    fdIntro.right = new FormAttachment(100, 0);
+    wIntro.setLayoutData(fdIntro);
+    Control last = wIntro;
+
+    last = addInfoRow(last, margin, "JdbcDriverDownloadDialog.Field.Artifact", driverCoordinate());
+    last =
+        addInfoRow(
+            last,
+            margin,
+            "JdbcDriverDownloadDialog.Field.License",
+            download.getLicenseName() + " (category " + download.getLicenseCategory() + ")");
+    last =
+        addLinkRow(
+            last, margin, "JdbcDriverDownloadDialog.Field.LicenseUrl", download.getLicenseUrl());
+    last =
+        addLinkRow(last, margin, "JdbcDriverDownloadDialog.Field.Vendor", download.getVendorUrl());
+    last =
+        addInfoRow(
+            last, margin, "JdbcDriverDownloadDialog.Field.Source", DriverResolver.MAVEN_CENTRAL);
+
+    if (download.getNotes() != null) {
+      last = addInfoRow(last, margin, "JdbcDriverDownloadDialog.Field.Notes", download.getNotes());
+    }
+
+    // License acceptance checkbox (restricted drivers only)
+    if (restricted) {
+      wAccept = new Button(shell, SWT.CHECK);
+      PropsUi.setLook(wAccept);
+      wAccept.setText(
+          BaseMessages.getString(
+              PKG, "JdbcDriverDownloadDialog.AcceptLicense", download.getLicenseName()));
+      FormData fdAccept = new FormData();
+      fdAccept.top = new FormAttachment(last, margin * 2);
+      fdAccept.left = new FormAttachment(0, 0);
+      fdAccept.right = new FormAttachment(100, 0);
+      wAccept.setLayoutData(fdAccept);
+      wAccept.addListener(SWT.Selection, e -> wDownload.setEnabled(wAccept.getSelection()));
+      last = wAccept;
+    }
+
+    // Buttons
+    wDownload = new Button(shell, SWT.PUSH);
+    wDownload.setText(BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Button.Download"));
+    wDownload.setEnabled(!restricted);
+    Button wCancel = new Button(shell, SWT.PUSH);
+    wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel"));
+
+    BaseTransformDialog.positionBottomButtons(
+        shell, new Button[] {wDownload, wCancel}, margin, last);
+
+    wDownload.addListener(SWT.Selection, e -> doDownload());
+    wCancel.addListener(SWT.Selection, e -> dispose());
+
+    BaseDialog.defaultShellHandling(shell, c -> doDownload(), c -> dispose());
+    return installed;
+  }
+
+  private String driverCoordinate() {
+    return download.getMavenCoordinate() + ":" + download.getDefaultVersion();
+  }
+
+  /** A label + value row of plain text. */
+  private Control addInfoRow(Control top, int margin, String labelKey, String value) {
+    Label wLabel = new Label(shell, SWT.RIGHT);
+    PropsUi.setLook(wLabel);
+    wLabel.setText(BaseMessages.getString(PKG, labelKey));
+    FormData fdl = new FormData();
+    fdl.top = new FormAttachment(top, margin);
+    fdl.left = new FormAttachment(0, 0);
+    fdl.right = new FormAttachment(props.getMiddlePct(), -margin);
+    wLabel.setLayoutData(fdl);
+
+    Label wValue = new Label(shell, SWT.WRAP);
+    PropsUi.setLook(wValue);
+    wValue.setText(value == null ? "" : value);
+    FormData fdv = new FormData();
+    fdv.top = new FormAttachment(top, margin);
+    fdv.left = new FormAttachment(props.getMiddlePct(), 0);
+    fdv.right = new FormAttachment(100, 0);
+    wValue.setLayoutData(fdv);
+    return wValue;
+  }
+
+  /** A label + clickable link row. */
+  private Control addLinkRow(Control top, int margin, String labelKey, String url) {
+    Label wLabel = new Label(shell, SWT.RIGHT);
+    PropsUi.setLook(wLabel);
+    wLabel.setText(BaseMessages.getString(PKG, labelKey));
+    FormData fdl = new FormData();
+    fdl.top = new FormAttachment(top, margin);
+    fdl.left = new FormAttachment(0, 0);
+    fdl.right = new FormAttachment(props.getMiddlePct(), -margin);
+    wLabel.setLayoutData(fdl);
+
+    Link wLink = new Link(shell, SWT.NONE);
+    PropsUi.setLook(wLink);
+    String safeUrl = url == null ? "" : url;
+    wLink.setText(safeUrl.isEmpty() ? "" : "<a>" + safeUrl + "</a>");
+    wLink.addListener(SWT.Selection, e -> Program.launch(safeUrl));
+    FormData fdv = new FormData();
+    fdv.top = new FormAttachment(top, margin);
+    fdv.left = new FormAttachment(props.getMiddlePct(), 0);
+    fdv.right = new FormAttachment(100, 0);
+    wLink.setLayoutData(fdv);
+    return wLink;
+  }
+
+  private void doDownload() {
+    if (download.isRestricted() && (wAccept == null || !wAccept.getSelection())) {
+      return; // license not accepted yet
+    }
+
+    File target = DriverInstaller.defaultInstallFolder();
+    final List<File> result = new ArrayList<>();
+    IRunnableWithProgress op =
+        monitor -> {
+          monitor.beginTask(
+              BaseMessages.getString(
+                  PKG, "JdbcDriverDownloadDialog.Progress.Downloading", driverName),
+              1);
+          try {
+            result.addAll(new DriverInstaller().install(download, null, null, target));
+          } catch (Exception ex) {
+            throw new InvocationTargetException(ex, ex.getMessage());
+          }
+          monitor.worked(1);
+          monitor.done();
+        };
+
+    try {
+      ProgressMonitorDialog pmd = new ProgressMonitorDialog(shell);
+      pmd.run(true, op);
+    } catch (InvocationTargetException | InterruptedException ex) {
+      new ErrorDialog(
+          shell,
+          BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Error.Title"),
+          BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Error.Message", driverName),
+          ex);
+      if (ex instanceof InterruptedException) {
+        Thread.currentThread().interrupt();
+      }
+      return;
+    }
+
+    installed = true;
+
+    // Try to hot-load so no restart is needed.
+    boolean hotLoaded = DriverInstaller.hotLoad(databaseType, result);
+
+    MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
+    box.setText(BaseMessages.getString(PKG, "JdbcDriverDownloadDialog.Done.Title"));
+    box.setMessage(
+        BaseMessages.getString(
+            PKG,
+            hotLoaded
+                ? "JdbcDriverDownloadDialog.Done.MessageReady"
+                : "JdbcDriverDownloadDialog.Done.Message",
+            Integer.toString(result.size()),
+            target.getAbsolutePath(),
+            driverName));
+    box.open();
+    dispose();
+  }
+
+  private void dispose() {
+    props.setScreen(new WindowProperty(shell));
+    shell.dispose();
+  }
+}
diff --git a/ui/src/main/resources/org/apache/hop/ui/core/database/messages/messages_en_US.properties b/ui/src/main/resources/org/apache/hop/ui/core/database/messages/messages_en_US.properties
index a0eefc2449..563576b233 100644
--- a/ui/src/main/resources/org/apache/hop/ui/core/database/messages/messages_en_US.properties
+++ b/ui/src/main/resources/org/apache/hop/ui/core/database/messages/messages_en_US.properties
@@ -16,7 +16,26 @@
 #
 
 DatabaseDialog.AdvancedTab.title=Advanced
+DatabaseDialog.button.DownloadDriver=Download driver
 DatabaseDialog.button.Explore=Explore
+JdbcDriverDownloadDialog.Shell.Title=Download JDBC driver
+JdbcDriverDownloadDialog.Intro.Restricted=Download the {0} JDBC driver from Maven Central. Accept the vendor license below to continue.
+JdbcDriverDownloadDialog.Intro.Open=Download the {0} JDBC driver from Maven Central.
+JdbcDriverDownloadDialog.Field.Artifact=Artifact
+JdbcDriverDownloadDialog.Field.License=License
+JdbcDriverDownloadDialog.Field.LicenseUrl=License terms
+JdbcDriverDownloadDialog.Field.Vendor=Vendor
+JdbcDriverDownloadDialog.Field.Source=Download source
+JdbcDriverDownloadDialog.Field.Notes=Notes
+JdbcDriverDownloadDialog.AcceptLicense=I have read and accept the {0} license terms
+JdbcDriverDownloadDialog.Button.Download=Download
+JdbcDriverDownloadDialog.Progress.Downloading=Downloading the {0} JDBC driver from Maven Central...
+JdbcDriverDownloadDialog.Error.Title=Driver download error
+JdbcDriverDownloadDialog.Error.Message=Unable to download the {0} JDBC driver.
+JdbcDriverDownloadDialog.Error.Generic=Unable to download the JDBC driver.
+JdbcDriverDownloadDialog.Done.Title=Driver installed
+JdbcDriverDownloadDialog.Done.Message=Installed {0} file(s) into {1}.\n\nRestart Hop for the {2} driver to be loaded.
+JdbcDriverDownloadDialog.Done.MessageReady=Installed {0} file(s) into {1}.\n\nThe {2} driver is ready to use - no restart needed.
 DatabaseDialog.column.Parameter=Parameter
 DatabaseDialog.column.Value=Value
 DatabaseDialog.DatabaseConnectionTest.title=Database Connection Test