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

jongyoul pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 206b6a319d [ZEPPELIN-6208] Enable DuckDB support in JDBC Interpreter 
by excluding incompatible default properties
206b6a319d is described below

commit 206b6a319dd4e30bf8c5df7331847f440724fff0
Author: HyeonUk Kang <[email protected]>
AuthorDate: Sun Aug 9 22:53:41 2026 +0900

    [ZEPPELIN-6208] Enable DuckDB support in JDBC Interpreter by excluding 
incompatible default properties
    
     What is this PR for?
    
      The JDBC interpreter forwards all of an interpreter's prefixed properties 
(default.*) straight to the JDBC driver when opening a connection. Many of 
these keys are consumed by Zeppelin itself or by the DBCP connection pool — 
e.g. driver, url, precode, statementPrecode,
      completer.ttlInSeconds, validationQuery, maxIdle — and are not valid JDBC 
connection properties.
    
      Lenient drivers (PostgreSQL, MySQL) silently ignore unknown properties, 
so this went unnoticed. But strict drivers reject the connection outright. For 
example, DuckDB fails with:
    
      SQLException: Invalid Input Error: The following options were not 
recognized:
                    completer.ttlInSeconds, url, driver, maxIdle
    
      This currently makes it impossible to use DuckDB (and other strict 
drivers such as MS SQL Server) with the JDBC interpreter.
    
      Until now this was worked around with a driver-specific whitelist that 
only applied when the driver class was Presto/Trino (PRESTO_PROPERTIES). That 
approach is hard to maintain: every new strict driver needs its own if branch 
and allow-list, and it also strips any legitimate driver
      property the user added but that wasn't on the list.
    
      This PR replaces that with a generic, driver-agnostic deny-list:
    
      - Introduces NON_DRIVER_PROPERTIES, a single source of truth listing only 
Zeppelin-internal and DBCP pool keys (user/password are deliberately kept, 
since they are standard JDBC properties).
      - Adds toDriverProperties(), which returns a filtered copy of the 
properties handed to the driver, leaving the original untouched (the previous 
Presto code mutated the shared per-user Properties in place — a latent bug).
      - Removes the Presto/Trino special-case branch and the PRESTO_PROPERTIES 
whitelist; the generic filter subsumes it.
      - Adds an optional escape hatch, zeppelin.jdbc.driver.excludeProperties 
(comma-separated), so operators can exclude additional keys for future drivers 
without a code change.
    
      Result: DuckDB, MS SQL Server, Trino/Presto, and any future strict driver 
work through one consistent rule, while genuine driver properties (e.g. SSL, 
useSSL, sslmode) still pass through unchanged.
    
      What type of PR is it?
    
      Bug Fix
    
      Todos
    
      - [x] Replace Presto/Trino whitelist with a generic internal-property 
deny-list
      - [x] Keep user/password and arbitrary driver properties (e.g. SSL) 
flowing to the driver
      - [x] Add unit tests for the filtering logic
      - [x] Add end-to-end tests against real strict drivers (DuckDB embedded, 
Trino)
    
    | Property setting (1) | Property setting (2) |
      |:---:|:---:|
      | <img width="2507" height="963" alt="스크린샷 2026-06-25 오후 9 20 23" 
src="https://github.com/user-attachments/assets/9c32bab8-d6d5-4096-b90a-5fa59e434509";
 /> | <img width="2083" height="139" alt="스크린샷 2026-06-25 오후 9 20 29" 
src="https://github.com/user-attachments/assets/2e0d2320-59eb-4efb-b28d-0f9bf5e7bc5f";
 /> |
    
      **2. Result — Before / After**
    
      | Before — DuckDB connection error | After — connected successfully |
      |:---:|:---:|
      | <img width="2462" height="206" alt="before-duckdb-error" 
src="https://github.com/user-attachments/assets/9b204002-9373-44a1-8d9b-1e41da30e4f6";
 /> | <img width="2475" height="517" alt="after-success" 
src="https://github.com/user-attachments/assets/417141ca-f780-4e58-8f55-0c32f9d6102c";
 />
    
    
      What is the Jira issue?
    
      - [ZEPPELIN-6208](https://issues.apache.org/jira/browse/ZEPPELIN-6208)
    
      How should this be tested?
    
      Automated tests added in JDBCInterpreterTest:
    
      - testToDriverProperties — internal/pool keys are removed; user, 
password, and arbitrary driver props (SSL) are kept; the original Properties is 
not mutated.
      - testToDriverPropertiesWithUserDefinedExcludes — 
zeppelin.jdbc.driver.excludeProperties strips additional user-specified keys.
      - testDuckDbConnectionWithInternalProperties — end-to-end: configures the 
interpreter with internal keys present and runs CREATE/INSERT/SELECT against an 
embedded DuckDB (no server needed). Fails on the old code, passes here.
      - testTrinoConnectionWithInternalProperties — end-to-end against a real 
Trino coordinator; auto-skipped via assumeTrue when none is reachable on 
localhost:8080.
    
      mvn -pl jdbc -am test -Dtest=JDBCInterpreterTest
    
      Manual: add a JDBC interpreter with 
default.driver=org.duckdb.DuckDBDriver, default.url=jdbc:duckdb:, add the 
org.duckdb:duckdb_jdbc dependency in the interpreter settings, leave an 
internal key such as default.completer.ttlInSeconds=120, and run a query — it 
now connects successfully.
    
    
    
      Questions:
    
      - Does the license files need to update? - X
      - Is there breaking changes for older versions? - X
      - Does this need documentation? - Optional
    
    Closes #5276 from hyunw9/ZEPPELIN-6208.
    
    Signed-off-by: Jongyoul Lee <[email protected]>
---
 jdbc/pom.xml                                       |  16 +++
 .../org/apache/zeppelin/jdbc/JDBCInterpreter.java  |  73 +++++++----
 .../apache/zeppelin/jdbc/JDBCInterpreterTest.java  | 142 +++++++++++++++++++++
 3 files changed, 209 insertions(+), 22 deletions(-)

diff --git a/jdbc/pom.xml b/jdbc/pom.xml
index 7225529055..67cba4d95d 100644
--- a/jdbc/pom.xml
+++ b/jdbc/pom.xml
@@ -43,6 +43,8 @@
 
     <!--test library versions-->
     <mockrunner.jdbc.version>1.0.8</mockrunner.jdbc.version>
+    <duckdb.jdbc.version>1.3.1.0</duckdb.jdbc.version>
+    <trino.jdbc.version>481</trino.jdbc.version>
   </properties>
 
   <dependencies>
@@ -67,6 +69,20 @@
       <scope>test</scope>
     </dependency>
 
+    <dependency>
+      <groupId>org.duckdb</groupId>
+      <artifactId>duckdb_jdbc</artifactId>
+      <version>${duckdb.jdbc.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>io.trino</groupId>
+      <artifactId>trino-jdbc</artifactId>
+      <version>${trino.jdbc.version}</version>
+      <scope>test</scope>
+    </dependency>
+
     <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-lang3</artifactId>
diff --git a/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCInterpreter.java 
b/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCInterpreter.java
index 90c34614b5..aa486fec43 100644
--- a/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCInterpreter.java
+++ b/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCInterpreter.java
@@ -147,14 +147,29 @@ public class JDBCInterpreter extends KerberosInterpreter {
   private static final String DBCP_STRING = "jdbc:apache:commons:dbcp:";
   private static final String MAX_ROWS_KEY = "zeppelin.jdbc.maxRows";
 
-  private static final Set<String> PRESTO_PROPERTIES = new 
HashSet<>(Arrays.asList(
-          "user", "password",
-          "socksProxy", "httpProxy", "clientTags", "applicationNamePrefix", 
"accessToken",
-          "SSL", "SSLKeyStorePath", "SSLKeyStorePassword", "SSLTrustStorePath",
-          "SSLTrustStorePassword", "KerberosRemoteServiceName", 
"KerberosPrincipal",
-          "KerberosUseCanonicalHostname", "KerberosServicePrincipalPattern",
-          "KerberosConfigPath", "KerberosKeytabPath", 
"KerberosCredentialCachePath",
-          "extraCredentials", "roles", "sessionProperties"));
+  /**
+   * Properties that Zeppelin consumes internally (or hands to the DBCP 
connection
+   * pool) and therefore must NOT be forwarded to the JDBC driver as connection
+   * properties.
+   *
+   * Note: "user" and "password" are deliberately excluded from this set 
because
+   * they are standard JDBC connection properties and must reach the driver.
+   */
+  private static final Set<String> NON_DRIVER_PROPERTIES = new 
HashSet<>(Arrays.asList(
+          // connection metadata consumed by the interpreter itself
+          DRIVER_KEY, URL_KEY,
+          // SQL hooks executed by the interpreter, not the driver
+          PRECODE_KEY, STATEMENT_PRECODE_KEY,
+          // auto-completion settings
+          COMPLETER_TTL_KEY, COMPLETER_SCHEMA_FILTERS_KEY,
+          // proxy / credential settings handled by the interpreter
+          "proxy.user.property", JDBC_JCEKS_FILE, JDBC_JCEKS_CREDENTIAL_KEY,
+          // DBCP connection-pool settings applied in configConnectionPool()
+          "validationQuery", "testOnBorrow", "testOnCreate", "testOnReturn",
+          "testWhileIdle", "timeBetweenEvictionRunsMillis", "maxWaitMillis",
+          "maxIdle", "minIdle", "maxTotal"));
+
+  static final String DRIVER_EXCLUDE_PROPERTIES_KEY = 
"zeppelin.jdbc.driver.excludeProperties";
 
   private static final String ALLOW_LOAD_LOCAL = "allowLoadLocal";
 
@@ -486,28 +501,42 @@ public class JDBCInterpreter extends KerberosInterpreter {
     connectionPool.setMaxWaitMillis(maxWaitMillis);
   }
 
+  /**
+   * Builds the property set handed to the JDBC driver: a copy of {@code 
properties}
+   * with all Zeppelin-internal and connection-pool keys ({@link 
#NON_DRIVER_PROPERTIES})
+   * removed. The original is left untouched so it can still be used for pool
+   * configuration and lookups. Additional keys can be excluded via
+   * {@value #DRIVER_EXCLUDE_PROPERTIES_KEY} (comma-separated).
+   */
+  // package private for testing purposes
+  Properties toDriverProperties(Properties properties) {
+    Set<String> excludes = new HashSet<>(NON_DRIVER_PROPERTIES);
+    String userExcludes = getProperty(DRIVER_EXCLUDE_PROPERTIES_KEY);
+    if (StringUtils.isNotBlank(userExcludes)) {
+      for (String key : userExcludes.split(",")) {
+        excludes.add(key.trim());
+      }
+    }
+
+    Properties driverProperties = new Properties();
+    for (String key : properties.stringPropertyNames()) {
+      if (!excludes.contains(key)) {
+        driverProperties.setProperty(key, properties.getProperty(key));
+      }
+    }
+    return driverProperties;
+  }
+
   private void createConnectionPool(String url, String user,
       Properties properties) throws SQLException, ClassNotFoundException {
 
     LOGGER.info("Creating connection pool for url: {}, user: {}", url, user);
 
-    /* Remove properties that is not valid properties for presto/trino by 
checking driver key.
-     * - Presto: com.facebook.presto.jdbc.PrestoDriver
-     * - Trino(ex. PrestoSQL): io.trino.jdbc.TrinoDriver / 
io.prestosql.jdbc.PrestoDriver
-     */
     String driverClass = properties.getProperty(DRIVER_KEY);
-    if (driverClass != null && 
(driverClass.equals("com.facebook.presto.jdbc.PrestoDriver")
-            || driverClass.equals("io.prestosql.jdbc.PrestoDriver")
-            || driverClass.equals("io.trino.jdbc.TrinoDriver"))) {
-      for (String key : properties.stringPropertyNames()) {
-        if (!PRESTO_PROPERTIES.contains(key)) {
-          properties.remove(key);
-        }
-      }
-    }
+    Properties driverProperties = toDriverProperties(properties);
 
     ConnectionFactory connectionFactory =
-            new DriverManagerConnectionFactory(url, properties);
+            new DriverManagerConnectionFactory(url, driverProperties);
 
     PoolableConnectionFactory poolableConnectionFactory = new 
PoolableConnectionFactory(
             connectionFactory, null);
diff --git 
a/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterTest.java 
b/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterTest.java
index cc5002b22a..f688e81310 100644
--- a/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterTest.java
+++ b/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterTest.java
@@ -59,12 +59,14 @@ import static 
org.apache.zeppelin.jdbc.JDBCInterpreter.DEFAULT_PRECODE;
 import static 
org.apache.zeppelin.jdbc.JDBCInterpreter.DEFAULT_STATEMENT_PRECODE;
 import static org.apache.zeppelin.jdbc.JDBCInterpreter.DEFAULT_URL;
 import static org.apache.zeppelin.jdbc.JDBCInterpreter.DEFAULT_USER;
+import static 
org.apache.zeppelin.jdbc.JDBCInterpreter.DRIVER_EXCLUDE_PROPERTIES_KEY;
 import static org.apache.zeppelin.jdbc.JDBCInterpreter.PRECODE_KEY_TEMPLATE;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 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 static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 
 /**
@@ -189,6 +191,146 @@ public class JDBCInterpreterTest extends 
BasicJDBCTestCaseAdapter {
     assertEquals("1000", jdbcInterpreter.getProperty(COMMON_MAX_LINE));
   }
 
+  @Test
+  void testToDriverProperties() {
+    JDBCInterpreter jdbcInterpreter = new JDBCInterpreter(new Properties());
+
+    Properties properties = new Properties();
+    // genuine JDBC driver properties, must be kept
+    properties.setProperty("user", "trino_user");
+    properties.setProperty("password", "secret");
+    properties.setProperty("SSL", "true");
+    // Zeppelin-internal / pool properties, must be removed (ZEPPELIN-6208)
+    properties.setProperty("driver", "io.trino.jdbc.TrinoDriver");
+    properties.setProperty("url", "jdbc:trino://localhost:8080");
+    properties.setProperty("precode", "set time zone 'UTC'");
+    properties.setProperty("statementPrecode", "set time zone 'UTC'");
+    properties.setProperty("completer.ttlInSeconds", "120");
+    properties.setProperty("completer.schemaFilters", "public");
+    properties.setProperty("validationQuery", "show databases");
+    properties.setProperty("maxIdle", "8");
+
+    Properties driverProperties = 
jdbcInterpreter.toDriverProperties(properties);
+
+    assertEquals(3, driverProperties.size());
+    assertEquals("trino_user", driverProperties.getProperty("user"));
+    assertEquals("secret", driverProperties.getProperty("password"));
+    assertEquals("true", driverProperties.getProperty("SSL"));
+    assertFalse(driverProperties.containsKey("driver"));
+    assertFalse(driverProperties.containsKey("url"));
+    assertFalse(driverProperties.containsKey("precode"));
+    assertFalse(driverProperties.containsKey("statementPrecode"));
+    assertFalse(driverProperties.containsKey("completer.ttlInSeconds"));
+    assertFalse(driverProperties.containsKey("completer.schemaFilters"));
+    assertFalse(driverProperties.containsKey("validationQuery"));
+    assertFalse(driverProperties.containsKey("maxIdle"));
+
+    // the original properties must not be mutated
+    assertTrue(properties.containsKey("driver"));
+    assertTrue(properties.containsKey("url"));
+  }
+
+  @Test
+  void testToDriverPropertiesWithUserDefinedExcludes() {
+    Properties config = new Properties();
+    config.setProperty(DRIVER_EXCLUDE_PROPERTIES_KEY, "SSL, customKey");
+    JDBCInterpreter jdbcInterpreter = new JDBCInterpreter(config);
+
+    Properties properties = new Properties();
+    properties.setProperty("user", "trino_user");
+    properties.setProperty("SSL", "true");
+    properties.setProperty("customKey", "customValue");
+
+    Properties driverProperties = 
jdbcInterpreter.toDriverProperties(properties);
+
+    assertEquals(1, driverProperties.size());
+    assertEquals("trino_user", driverProperties.getProperty("user"));
+    assertFalse(driverProperties.containsKey("SSL"));
+    assertFalse(driverProperties.containsKey("customKey"));
+  }
+
+  /**
+   * End-to-end check that a strict JDBC driver (DuckDB) can connect even 
though
+   * Zeppelin-internal properties are present in the interpreter configuration.
+   * Before ZEPPELIN-6208 those keys were forwarded to the driver and DuckDB
+   * rejected the connection. DuckDB runs in-process, so no external server is
+   * needed.
+   */
+  @Test
+  void testDuckDbConnectionWithInternalProperties()
+      throws IOException, InterpreterException {
+    Properties properties = new Properties();
+    properties.setProperty("default.driver", "org.duckdb.DuckDBDriver");
+    properties.setProperty("default.url", "jdbc:duckdb:");
+    properties.setProperty("default.user", "");
+    properties.setProperty("default.password", "");
+    // Internal keys that DuckDB's strict driver would reject if forwarded
+    properties.setProperty("default.completer.ttlInSeconds", "120");
+    properties.setProperty("default.completer.schemaFilters", "");
+    properties.setProperty("common.max_count", "1000");
+    JDBCInterpreter t = new JDBCInterpreter(properties);
+    t.open();
+
+    String sqlQuery = "CREATE TABLE pokes (id INTEGER, name VARCHAR); " +
+        "INSERT INTO pokes VALUES (1, 'a'), (2, 'b'); " +
+        "SELECT * FROM pokes ORDER BY id;";
+    InterpreterResult interpreterResult = t.interpret(sqlQuery, context);
+
+    assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code());
+    List<InterpreterResultMessage> resultMessages = 
context.out.toInterpreterResultMessage();
+    InterpreterResultMessage tableMessage = resultMessages.stream()
+        .filter(m -> m.getType() == InterpreterResult.Type.TABLE)
+        .reduce((first, second) -> second)
+        .orElseThrow(() -> new AssertionError("No TABLE result produced"));
+    assertEquals("id\tname\n1\ta\n2\tb\n", tableMessage.getData());
+  }
+
+  private static boolean isTrinoAvailable(String host, int port) {
+    try (java.net.Socket socket = new java.net.Socket()) {
+      socket.connect(new java.net.InetSocketAddress(host, port), 1000);
+      return true;
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
+  /**
+   * End-to-end check against a real Trino coordinator. Like DuckDB, Trino has 
a
+   * strict driver that rejects unknown connection properties; before 
ZEPPELIN-6208
+   * these were stripped by a driver-specific whitelist, now by the generic 
filter.
+   * Skipped automatically when no Trino server is reachable on localhost:8080.
+   */
+  @Test
+  void testTrinoConnectionWithInternalProperties()
+      throws IOException, InterpreterException {
+    assumeTrue(isTrinoAvailable("localhost", 8080),
+        "Trino coordinator not reachable on localhost:8080, skipping");
+
+    Properties properties = new Properties();
+    properties.setProperty("default.driver", "io.trino.jdbc.TrinoDriver");
+    properties.setProperty("default.url", "jdbc:trino://localhost:8080");
+    properties.setProperty("default.user", "test");
+    properties.setProperty("default.password", "");
+    // Internal keys that Trino's strict driver would reject if forwarded
+    properties.setProperty("default.completer.ttlInSeconds", "120");
+    properties.setProperty("default.completer.schemaFilters", "");
+    properties.setProperty("common.max_count", "1000");
+    JDBCInterpreter t = new JDBCInterpreter(properties);
+    t.open();
+
+    String sqlQuery =
+        "SELECT nationkey AS id, name FROM tpch.tiny.nation ORDER BY nationkey 
LIMIT 2;";
+    InterpreterResult interpreterResult = t.interpret(sqlQuery, context);
+
+    assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code());
+    List<InterpreterResultMessage> resultMessages = 
context.out.toInterpreterResultMessage();
+    InterpreterResultMessage tableMessage = resultMessages.stream()
+        .filter(m -> m.getType() == InterpreterResult.Type.TABLE)
+        .reduce((first, second) -> second)
+        .orElseThrow(() -> new AssertionError("No TABLE result produced"));
+    assertEquals("id\tname\n0\tALGERIA\n1\tARGENTINA\n", 
tableMessage.getData());
+  }
+
   @Test
   void testSelectQuery() throws IOException, InterpreterException {
     Properties properties = new Properties();

Reply via email to