From 5b61f17c49f6f13d2f302f934ee0341cb4adf8ee Mon Sep 17 00:00:00 2001 From: zipg Date: Wed, 26 Aug 2026 12:43:31 +0800 Subject: [PATCH] feat(dameng): support dm6 connections via custom jdbc drivers --- .../java/com/dbx/agent/AbstractJdbcAgent.java | 32 ++- .../dbx/agent/JdbcConnectionPoolRegistry.java | 13 +- .../java/com/dbx/agent/JsonRpcServer.java | 12 +- .../com/dbx/agent/AbstractJdbcAgentTest.java | 23 ++ .../com/dbx/agent/dameng/DamengAgent.java | 196 +++++++++++++++++- .../agent/dameng/DamengAgentMetadataTest.java | 76 +++++++ .../com/dbx/agent/dameng/DamengAgentTest.java | 39 ++++ .../dbx/agent/dameng/DamengAgentUrlTest.java | 18 ++ .../connection/ConnectionDialog.vue | 80 ++++++- apps/desktop/src/i18n/locales/en.ts | 5 + apps/desktop/src/i18n/locales/es.ts | 5 + apps/desktop/src/i18n/locales/it.ts | 5 + apps/desktop/src/i18n/locales/ja.ts | 5 + apps/desktop/src/i18n/locales/ko.ts | 5 + apps/desktop/src/i18n/locales/pt-BR.ts | 5 + apps/desktop/src/i18n/locales/zh-CN.ts | 5 + apps/desktop/src/i18n/locales/zh-TW.ts | 5 + .../bigqueryConnectionDialog.spec.ts | 2 +- .../database/damengDriverOptions.spec.ts | 23 ++ .../src/lib/database/damengDriverOptions.ts | 27 +++ 20 files changed, 560 insertions(+), 21 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/database/damengDriverOptions.spec.ts create mode 100644 apps/desktop/src/lib/database/damengDriverOptions.ts diff --git a/agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java b/agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java index 0c5044e414..61cad50dba 100644 --- a/agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java +++ b/agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java @@ -90,7 +90,7 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent { return unchecked(() -> { loadDriver(params); try (Connection conn = openTestConnection(params)) { - boolean valid = conn != null && conn.isValid(5); + boolean valid = isConnectionValid(conn, 5); Map result = new LinkedHashMap<>(); result.put("ok", valid); if (valid) { @@ -275,6 +275,31 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent { return true; } + final boolean isConnectionValid(Connection connection, int timeoutSecs) { + if (connection == null) { + return false; + } + try { + if (connection.isClosed()) { + return false; + } + String validationQuery = connectionValidationQuery(); + if (validationQuery == null || validationQuery.isBlank()) { + return connection.isValid(timeoutSecs); + } + try (Statement statement = connection.createStatement()) { + try { + statement.setQueryTimeout(timeoutSecs); + } catch (Exception | AbstractMethodError ignored) { + } + statement.execute(validationQuery); + return true; + } + } catch (Exception | AbstractMethodError ignored) { + return false; + } + } + final synchronized boolean usesConnectionPool() { return poolRegistry != null; } @@ -485,6 +510,10 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent { protected void afterDisconnect() throws Exception { } + protected String connectionValidationQuery() { + return null; + } + protected void beforeQueryExecution(Connection connection, int timeoutSecs) throws Exception { } @@ -586,6 +615,7 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent { return registry.borrow( identity, JdbcSessionRole.from(params.getSessionRole()), + connectionValidationQuery(), () -> openInitializedConnection(params) ); } diff --git a/agents/common/src/main/java/com/dbx/agent/JdbcConnectionPoolRegistry.java b/agents/common/src/main/java/com/dbx/agent/JdbcConnectionPoolRegistry.java index 514dc1405a..a9e5594ade 100644 --- a/agents/common/src/main/java/com/dbx/agent/JdbcConnectionPoolRegistry.java +++ b/agents/common/src/main/java/com/dbx/agent/JdbcConnectionPoolRegistry.java @@ -81,10 +81,14 @@ final class JdbcConnectionPoolRegistry implements AutoCloseable { } Lease borrow(String identity, ConnectionFactory connectionFactory) throws Exception { - return borrow(identity, JdbcSessionRole.WORKLOAD, connectionFactory); + return borrow(identity, JdbcSessionRole.WORKLOAD, null, connectionFactory); } Lease borrow(String identity, JdbcSessionRole role, ConnectionFactory connectionFactory) throws Exception { + return borrow(identity, role, null, connectionFactory); + } + + Lease borrow(String identity, JdbcSessionRole role, String connectionTestQuery, ConnectionFactory connectionFactory) throws Exception { String key = digest(identity); while (true) { if (closed.get()) { @@ -96,7 +100,7 @@ final class JdbcConnectionPoolRegistry implements AutoCloseable { } PoolEntry entry; try { - entry = pools.computeIfAbsent(key, ignored -> createPoolEntry(key, connectionFactory)); + entry = pools.computeIfAbsent(key, ignored -> createPoolEntry(key, connectionTestQuery, connectionFactory)); } catch (PoolCreationException error) { throw error.unwrap(); } @@ -130,7 +134,7 @@ final class JdbcConnectionPoolRegistry implements AutoCloseable { return entry != null && entry.hasActiveLeases(); } - private PoolEntry createPoolEntry(String key, ConnectionFactory connectionFactory) { + private PoolEntry createPoolEntry(String key, String connectionTestQuery, ConnectionFactory connectionFactory) { try { ConnectionFactoryDataSource factoryDataSource = new ConnectionFactoryDataSource( connectionFactory, @@ -146,6 +150,9 @@ final class JdbcConnectionPoolRegistry implements AutoCloseable { config.setMinimumIdle(settings.minimumIdle); config.setConnectionTimeout(settings.connectionTimeoutMillis); config.setValidationTimeout(settings.validationTimeoutMillis); + if (connectionTestQuery != null && !connectionTestQuery.isBlank()) { + config.setConnectionTestQuery(connectionTestQuery); + } config.setIdleTimeout(settings.idleTimeoutMillis); config.setMaxLifetime(settings.maxLifetimeMillis); config.setInitializationFailTimeout(-1L); diff --git a/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java b/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java index f88dc2d377..2e2f61c119 100644 --- a/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java +++ b/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java @@ -171,8 +171,10 @@ public final class JsonRpcServer { boolean valid = false; if (conn != null) { try { - valid = !conn.isClosed() && conn.isValid(2); - } catch (Exception ignored) { + valid = agent instanceof AbstractJdbcAgent jdbcAgent + ? jdbcAgent.isConnectionValid(conn, 2) + : !conn.isClosed() && conn.isValid(2); + } catch (Exception | AbstractMethodError ignored) { } } if (!valid) { @@ -370,8 +372,10 @@ public final class JsonRpcServer { } boolean valid = false; try { - valid = !conn.isClosed() && conn.isValid(2); - } catch (Exception ignored) { + valid = agent instanceof AbstractJdbcAgent validationAgent + ? validationAgent.isConnectionValid(conn, 2) + : !conn.isClosed() && conn.isValid(2); + } catch (Exception | AbstractMethodError ignored) { } if (valid) { lastConnectionValidationTimeMillis = now; diff --git a/agents/common/src/test/java/com/dbx/agent/AbstractJdbcAgentTest.java b/agents/common/src/test/java/com/dbx/agent/AbstractJdbcAgentTest.java index d3e2bc2e1e..54c2708844 100644 --- a/agents/common/src/test/java/com/dbx/agent/AbstractJdbcAgentTest.java +++ b/agents/common/src/test/java/com/dbx/agent/AbstractJdbcAgentTest.java @@ -127,6 +127,19 @@ class AbstractJdbcAgentTest { assertEquals(1, tracking.closeCount); } + @Test + void validatesLegacyJdbcConnectionsWithConfiguredQueryInsteadOfIsValid() { + TrackingConnection tracking = new TrackingConnection(); + tracking.isValidUnsupported = true; + TestAgent agent = new TestAgent(tracking); + agent.validationQuery = "SELECT 1"; + + assertTrue(agent.testConnection(new ConnectParams())); + + assertEquals(0, tracking.isValidCount); + assertEquals(Arrays.asList("setQueryTimeout:5", "execute:SELECT 1"), tracking.calls); + } + @Test void testConnectionCanSkipOpeningAPhysicalConnection() { TrackingConnection tracking = new TrackingConnection(); @@ -339,6 +352,7 @@ class AbstractJdbcAgentTest { private int afterConnectCount; private int afterDisconnectCount; private boolean skipTestConnectionOpen; + private String validationQuery; private TestAgent(TrackingConnection tracking) { this.tracking = tracking; @@ -375,6 +389,11 @@ class AbstractJdbcAgentTest { afterDisconnectCount += 1; } + @Override + protected String connectionValidationQuery() { + return validationQuery; + } + @Override public List listDatabases() { return Collections.emptyList(); @@ -436,6 +455,7 @@ class AbstractJdbcAgentTest { private String identifierQuote = "\""; private boolean compatibilityQueryFails; private int compatibilityQueryCount; + private boolean isValidUnsupported; private Connection connection() { return proxy(Connection.class, new MethodHandler() { @@ -450,6 +470,9 @@ class AbstractJdbcAgentTest { } if ("isValid".equals(name)) { isValidCount += 1; + if (isValidUnsupported) { + throw new AbstractMethodError("legacy JDBC driver"); + } return true; } if ("close".equals(name)) { diff --git a/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java b/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java index 985983b596..fee6b8b050 100644 --- a/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java +++ b/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java @@ -7,6 +7,7 @@ import com.dbx.agent.DatabaseInfo; import com.dbx.agent.ExecuteQueryOptions; import com.dbx.agent.ForeignKeyInfo; import com.dbx.agent.IndexInfo; +import com.dbx.agent.JdbcAgentProfile; import com.dbx.agent.JdbcExecutor; import com.dbx.agent.JdbcIdentifiers; import com.dbx.agent.MultiSessionJsonRpcServer; @@ -18,14 +19,18 @@ import com.dbx.agent.QueryPageResult; import com.dbx.agent.QueryResult; import com.dbx.agent.TableInfo; import com.dbx.agent.TriggerInfo; -import dm.jdbc.driver.DmdbConnection; +import com.dbx.agent.StandardJdbcMetadata; import java.io.PrintStream; import java.io.Reader; +import java.net.URL; +import java.net.URLClassLoader; import java.net.SocketTimeoutException; +import java.nio.file.Paths; import java.sql.Blob; import java.sql.Clob; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.Driver; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -49,16 +54,26 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public final class DamengAgent extends AbstractJdbcAgent { private static final Logger LOGGER = Logger.getLogger("com.dbx.agent.dameng"); private static final String AGENT_VERSION = "9999.06.04.1-fix-default"; private static final int DBMS_OUTPUT_ENABLE_TIMEOUT_SECS = 5; private static final int DBMS_OUTPUT_ENABLE_NETWORK_TIMEOUT_MILLIS = 5_000; + private static final Pattern DATABASE_VERSION_MAJOR_PATTERN = Pattern.compile("(\\d+)\\."); + private static final JdbcAgentProfile DM6_METADATA_PROFILE = new JdbcAgentProfile( + "dm6.jdbc.driver.DmDriver", + "jdbc:dm6://{host}:{port}/{database}", + 5236, + true + ); private static final String DAMENG_CLASSIFIED_OBJECT_TYPE_SQL = "CASE WHEN o.OBJECT_TYPE = 'MATERIALIZED VIEW' OR (o.OBJECT_TYPE = 'VIEW' AND mv.MVIEW_NAME IS NOT NULL) " + "THEN 'MATERIALIZED_VIEW' ELSE o.OBJECT_TYPE END"; @@ -90,6 +105,11 @@ public final class DamengAgent extends AbstractJdbcAgent { ) mv ON mv.OWNER = o.OWNER AND mv.MVIEW_NAME = o.OBJECT_NAME """.stripIndent().trim(); private String connectedUsername; + private Driver externalDriver; + private URLClassLoader externalDriverLoader; + private List externalDriverUrls; + private String externalDriverClass; + private volatile boolean legacyJdbcMetadata; private volatile boolean dbmsOutputInitializationSupported = true; private final Map dbmsOutputInitializedConnections = Collections.synchronizedMap(new WeakHashMap<>()); @@ -106,19 +126,120 @@ public final class DamengAgent extends AbstractJdbcAgent { @Override protected void loadDriver(ConnectParams params) throws Exception { - withSuppressedStdout(() -> super.loadDriver(params)); + withSuppressedStdout(() -> { + if (params.getJdbc_driver_paths() == null || params.getJdbc_driver_paths().isEmpty()) { + // Keep an existing external loader open: pooled connections may + // still hold classes loaded from it. Only stop using it here. + externalDriver = null; + super.loadDriver(params); + return; + } + + List urls = new ArrayList<>(); + for (String path : params.getJdbc_driver_paths()) { + urls.add(Paths.get(path).toUri().toURL()); + } + String driverClass = params.getJdbc_driver_class(); + if (driverClass == null || driverClass.trim().isEmpty()) { + driverClass = driverClass(); + } + // Reuse the loader for unchanged paths and class: recreating it + // would strand classes that live pooled connections still need + // (lazy driver classes would fail with NoClassDefFoundError). A + // loader dropped for changed paths is never closed here for the + // same reason; disconnect() releases the active one. + if (externalDriverLoader != null + && urls.equals(externalDriverUrls) + && driverClass.equals(externalDriverClass)) { + if (externalDriver == null) { + externalDriver = (Driver) Class.forName(driverClass, true, externalDriverLoader) + .getDeclaredConstructor() + .newInstance(); + } + return; + } + externalDriverLoader = new URLClassLoader( + urls.toArray(new URL[0]), + ClassLoader.getPlatformClassLoader() + ); + externalDriverUrls = urls; + externalDriverClass = driverClass; + externalDriver = (Driver) Class.forName(driverClass, true, externalDriverLoader) + .getDeclaredConstructor() + .newInstance(); + }); } @Override protected Connection openConnection(ConnectParams params) throws Exception { - return withSuppressedStdout( - () -> DriverManager.getConnection(buildUrl(params), params.getUsername(), params.getPassword()) - ); + return withSuppressedStdout(() -> { + if (externalDriver == null) { + return DriverManager.getConnection(buildUrl(params), params.getUsername(), params.getPassword()); + } + Properties properties = new Properties(); + if (params.getUsername() != null) { + properties.setProperty("user", params.getUsername()); + } + if (params.getPassword() != null) { + properties.setProperty("password", params.getPassword()); + } + Connection connection = externalDriver.connect(buildUrl(params), properties); + if (connection == null) { + throw new SQLException("Selected Dameng JDBC driver does not accept URL: " + buildUrl(params)); + } + return connection; + }); + } + + @Override + protected String connectionValidationQuery() { + return "SELECT 1"; + } + + @Override + public synchronized void disconnect() { + super.disconnect(); + try { + closeExternalDriverLoader(); + } catch (Exception error) { + // Best-effort release during teardown. + } + } + + private void closeExternalDriverLoader() throws Exception { + externalDriver = null; + externalDriverUrls = null; + externalDriverClass = null; + if (externalDriverLoader != null) { + externalDriverLoader.close(); + externalDriverLoader = null; + } } @Override protected void afterConnect(ConnectParams params, Connection connection) { connectedUsername = params.getUsername(); + legacyJdbcMetadata = usesLegacyJdbcMetadata(connection); + } + + static boolean usesLegacyJdbcMetadata(Connection connection) { + try { + int major = connection.getMetaData().getDatabaseMajorVersion(); + if (major > 0) { + return major < 7; + } + } catch (Exception | AbstractMethodError ignored) { + } + try { + String version = connection.getMetaData().getDatabaseProductVersion(); + if (version == null) { + return false; + } + Matcher matcher = DATABASE_VERSION_MAJOR_PATTERN.matcher(version); + return matcher.find() && Integer.parseInt(matcher.group(1)) < 7; + } catch (Exception | AbstractMethodError ignored) { + return false; + } } @Override @@ -127,12 +248,12 @@ public final class DamengAgent extends AbstractJdbcAgent { } private void initializeDbmsOutputIfNeeded(String sql) throws SQLException { - if (!dbmsOutputInitializationSupported || !isDbmsOutputStatement(sql)) { + if (legacyJdbcMetadata || !dbmsOutputInitializationSupported || !isDbmsOutputStatement(sql)) { return; } Connection connection = requireConnected(); - Object physicalConnection = connection.unwrap(DmdbConnection.class); + Object physicalConnection = physicalConnectionIdentity(connection); synchronized (dbmsOutputInitializedConnections) { if (dbmsOutputInitializedConnections.containsKey(physicalConnection)) { return; @@ -190,6 +311,22 @@ public final class DamengAgent extends AbstractJdbcAgent { || startsWithKeyword(sql, start, "EXECUTE"); } + private Object physicalConnectionIdentity(Connection connection) { + try { + String driverClassName = externalDriver == null ? driverClass() : externalDriver.getClass().getName(); + int packageSeparator = driverClassName.lastIndexOf('.'); + if (packageSeparator < 0) { + return connection; + } + String connectionClassName = driverClassName.substring(0, packageSeparator + 1) + "DmdbConnection"; + ClassLoader loader = externalDriverLoader == null ? getClass().getClassLoader() : externalDriverLoader; + Class connectionClass = Class.forName(connectionClassName, false, loader); + return connection.unwrap(connectionClass); + } catch (Exception | AbstractMethodError ignored) { + return connection; + } + } + private static boolean startsWithKeyword(String sql, int start, String keyword) { int end = start + keyword.length(); return end <= sql.length() @@ -321,11 +458,17 @@ public final class DamengAgent extends AbstractJdbcAgent { @Override public List listDatabases() { + if (legacyJdbcMetadata) { + return unchecked(() -> listJdbcSchemas().stream().map(DatabaseInfo::new).toList()); + } return unchecked(() -> listVisibleUsers().stream().map(DatabaseInfo::new).toList()); } @Override public List listSchemas() { + if (legacyJdbcMetadata) { + return StandardJdbcMetadata.INSTANCE.listSchemas(requireConnected(), DM6_METADATA_PROFILE); + } return unchecked(() -> { try { return listVisibleSchemas(); @@ -395,6 +538,9 @@ public final class DamengAgent extends AbstractJdbcAgent { } private List queryConstrainedTables(String schema, MetadataListConstraints constraints) { + if (legacyJdbcMetadata) { + return executeJdbcMetadataTables(schema, constraints); + } if (!constraints.includesTableLikeTypes()) { return List.of(); } @@ -913,6 +1059,9 @@ public final class DamengAgent extends AbstractJdbcAgent { } private List queryConstrainedObjects(String schema, MetadataListConstraints constraints) { + if (legacyJdbcMetadata) { + return executeJdbcMetadataObjects(schema, constraints); + } if (!includesSupportedObjectTypes(constraints)) { return List.of(); } @@ -1451,6 +1600,9 @@ public final class DamengAgent extends AbstractJdbcAgent { @Override public String getTableDdl(String schema, String table) { + if (legacyJdbcMetadata) { + return super.getTableDdl(schema, table); + } try { return unchecked(() -> { String sql = "SELECT /*+ PARALLEL(1) */ DBMS_METADATA.GET_DDL(?, ?, ?) FROM DUAL"; @@ -1486,6 +1638,15 @@ public final class DamengAgent extends AbstractJdbcAgent { @Override public List getColumns(String schema, String table) { + if (legacyJdbcMetadata) { + return StandardJdbcMetadata.INSTANCE.getColumns( + requireConnected(), + DM6_METADATA_PROFILE, + getConfiguredDatabase(), + schema, + table + ); + } return unchecked(() -> { Set pkColumns = new java.util.HashSet<>(); String pkSql = """ @@ -1590,6 +1751,15 @@ public final class DamengAgent extends AbstractJdbcAgent { @Override public List listIndexes(String schema, String table) { + if (legacyJdbcMetadata) { + return StandardJdbcMetadata.INSTANCE.listIndexes( + requireConnected(), + DM6_METADATA_PROFILE, + getConfiguredDatabase(), + schema, + table + ); + } return unchecked(() -> { List result = new ArrayList<>(); String sql = """ @@ -1630,6 +1800,9 @@ public final class DamengAgent extends AbstractJdbcAgent { @Override public List listForeignKeys(String schema, String table) { + if (legacyJdbcMetadata) { + return StandardJdbcMetadata.INSTANCE.listForeignKeys(requireConnected(), schema, table); + } return unchecked(() -> { List result = new ArrayList<>(); String sql = """ @@ -1661,6 +1834,9 @@ public final class DamengAgent extends AbstractJdbcAgent { @Override public List listTriggers(String schema, String table) { + if (legacyJdbcMetadata) { + return StandardJdbcMetadata.INSTANCE.listTriggers(schema, table); + } return unchecked(() -> { List result = new ArrayList<>(); String sql = """ @@ -1722,7 +1898,7 @@ public final class DamengAgent extends AbstractJdbcAgent { } String message = (String) value; return message.isEmpty() ? List.of() : message.lines().toList(); - } catch (Exception ignored) { + } catch (Exception | AbstractMethodError ignored) { return List.of(); } } @@ -1921,6 +2097,10 @@ public final class DamengAgent extends AbstractJdbcAgent { } private static String buildUrl(ConnectParams params) { + String connectionString = params.getConnection_string() == null ? "" : params.getConnection_string().trim(); + if (!connectionString.isEmpty()) { + return connectionString; + } String database = params.getDatabase() == null ? "" : params.getDatabase().trim(); String suffix = database.isEmpty() ? "" : "/" + database; String url = "jdbc:dm://" + params.getHost() + ":" + params.getPort() + suffix; diff --git a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java index c842f02068..fff38b4947 100644 --- a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java +++ b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java @@ -1,6 +1,7 @@ package com.dbx.agent.dameng; import com.dbx.agent.ColumnInfo; +import com.dbx.agent.DatabaseInfo; import com.dbx.agent.MetadataListConstraints; import com.dbx.agent.ObjectInfo; import com.dbx.agent.ObjectSource; @@ -25,6 +26,31 @@ import java.util.Arrays; import java.util.List; class DamengAgentMetadataTest { + @Test + void detectsLegacyDamengMetadataFromDatabaseVersion() { + Assertions.assertTrue(DamengAgent.usesLegacyJdbcMetadata(versionConnection(6, "6.0.2.79"))); + Assertions.assertFalse(DamengAgent.usesLegacyJdbcMetadata(versionConnection(8, "8.1.5.45"))); + Assertions.assertTrue(DamengAgent.usesLegacyJdbcMetadata(versionConnection(0, "DM Database Server x64 V6.0.2.79"))); + } + + @Test + void legacyDamengUsesJdbcMetadataWithoutQueryingDm8CatalogViews() { + DamengAgent agent = new DamengAgent(); + List metadataCalls = new ArrayList<>(); + TestSupport.setPrivateConnection(agent, legacyMetadataConnection(metadataCalls)); + setLegacyJdbcMetadata(agent, true); + + List databases = agent.listDatabases(); + List schemas = agent.listSchemas(); + List tables = agent.listTables("DBX_TEST"); + + Assertions.assertEquals(List.of("DBX_TEST", "SYSDBA"), databases.stream().map(DatabaseInfo::getName).toList()); + Assertions.assertEquals(List.of("DBX_TEST", "SYSDBA"), schemas); + Assertions.assertEquals(List.of("CONNECTION_SMOKE"), tables.stream().map(TableInfo::getName).toList()); + Assertions.assertTrue(metadataCalls.contains("getSchemas"), metadataCalls.toString()); + Assertions.assertTrue(metadataCalls.contains("getTables:DBX\\_TEST"), metadataCalls.toString()); + } + @Test void usesColumnCommentsMetadataQuery() { DamengAgent agent = new DamengAgent(); @@ -1335,6 +1361,56 @@ class DamengAgentMetadataTest { } } + private static void setLegacyJdbcMetadata(DamengAgent agent, boolean value) { + try { + Field field = DamengAgent.class.getDeclaredField("legacyJdbcMetadata"); + field.setAccessible(true); + field.set(agent, value); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to set legacy JDBC metadata mode", e); + } + } + + private static Connection versionConnection(int majorVersion, String productVersion) { + DatabaseMetaData metadata = proxy(DatabaseMetaData.class, (method, args) -> switch (method.getName()) { + case "getDatabaseMajorVersion" -> majorVersion; + case "getDatabaseProductVersion" -> productVersion; + default -> defaultValue(method.getReturnType()); + }); + return proxy(Connection.class, (method, args) -> + "getMetaData".equals(method.getName()) ? metadata : defaultValue(method.getReturnType()) + ); + } + + private static Connection legacyMetadataConnection(List calls) { + DatabaseMetaData metadata = proxy(DatabaseMetaData.class, (method, args) -> { + switch (method.getName()) { + case "getSchemas": + calls.add("getSchemas"); + return metadataResultSet(List.of(List.of("SYSDBA"), List.of("DBX_TEST"))); + case "getTableTypes": + return metadataResultSet(List.of(List.of("", "TABLE", ""))); + case "getTables": + Assertions.assertNull(args[3], "DM6 JDBC rejects non-null getTables types"); + calls.add("getTables:" + args[1]); + return metadataResultSet(List.of(List.of("CONNECTION_SMOKE", "TABLE", "smoke table"))); + case "getSearchStringEscape": + return "\\"; + default: + return defaultValue(method.getReturnType()); + } + }); + return proxy(Connection.class, (method, args) -> { + if ("getMetaData".equals(method.getName())) { + return metadata; + } + if ("prepareStatement".equals(method.getName())) { + throw new AssertionError("Legacy metadata mode must not query DM8 catalog views: " + args[0]); + } + return defaultValue(method.getReturnType()); + }); + } + private static Connection objectSourceConnection(List params, String source) { return proxy(Connection.class, (method, args) -> { String name = method.getName(); diff --git a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentTest.java b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentTest.java index c193272c2f..f935a86e74 100644 --- a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentTest.java +++ b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentTest.java @@ -12,6 +12,7 @@ import com.dbx.agent.test.JdbcAgentFake; import com.dbx.agent.test.TestSupport; import org.junit.jupiter.api.Test; +import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -100,6 +101,18 @@ class DamengAgentTest extends JdbcFakeExecutionBehaviorTest { assertDoesNotEnableDbmsOutput("/* CALL hidden in a comment */ SELECT 1"); } + @Test + void legacyDamengSkipsUnavailableDbmsOutputInitialization() { + List executedSql = new ArrayList<>(); + DamengAgent agent = new DamengAgent(); + TestSupport.setPrivateConnection(agent, printMessageConnection(null, executedSql)); + setLegacyJdbcMetadata(agent, true); + + agent.executeQuery("BEGIN NULL; END;", null, new ExecuteQueryOptions()); + + assertEquals(List.of("BEGIN NULL; END;"), executedSql); + } + @Test void outputStatementsEnableDbmsOutputOncePerPhysicalConnection() { List firstConnectionSql = new ArrayList<>(); @@ -216,6 +229,22 @@ class DamengAgentTest extends JdbcFakeExecutionBehaviorTest { ), executedSql); } + @Test + void legacyStatementWithoutJdbc4UnwrapReturnsNoPrintMessages() { + Statement statement = (Statement) Proxy.newProxyInstance( + DamengAgentTest.class.getClassLoader(), + new Class[]{Statement.class}, + (unused, method, args) -> { + if ("unwrap".equals(method.getName())) { + throw new AbstractMethodError("legacy JDBC driver"); + } + return defaultValue(method.getReturnType()); + } + ); + + assertEquals(List.of(), DamengAgent.statementPrintMessages(statement)); + } + @Test void pagedOutputStatementsEnableDbmsOutputLazily() { List executedSql = new ArrayList<>(); @@ -620,6 +649,16 @@ class DamengAgentTest extends JdbcFakeExecutionBehaviorTest { assertEquals(List.of("BEGIN DBMS_OUTPUT.ENABLE(1000000); END;"), executedSql); } + private static void setLegacyJdbcMetadata(DamengAgent agent, boolean value) { + try { + Field field = DamengAgent.class.getDeclaredField("legacyJdbcMetadata"); + field.setAccessible(true); + field.set(agent, value); + } catch (ReflectiveOperationException error) { + throw new IllegalStateException("Unable to set legacy JDBC metadata mode", error); + } + } + private static Object defaultValue(Class type) { if (type == Boolean.TYPE) return false; if (type == Byte.TYPE) return (byte) 0; diff --git a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentUrlTest.java b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentUrlTest.java index 8322c43087..6548d65c28 100644 --- a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentUrlTest.java +++ b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentUrlTest.java @@ -21,6 +21,24 @@ class DamengAgentUrlTest { Assertions.assertEquals("jdbc:dm://127.0.0.1:5236/MAIN", url); } + @Test + void usesCompleteCustomJdbcUrlWithoutRewritingIt() throws Exception { + ConnectParams params = new ConnectParams( + "ignored", + 5236, + "IGNORED", + "", + "", + "ssl=true", + "jdbc:dm6://dm6.internal:5237/MAIN?compatibleMode=oracle", + false + ); + + String url = invokeBuildUrl(params); + + Assertions.assertEquals("jdbc:dm6://dm6.internal:5237/MAIN?compatibleMode=oracle", url); + } + @Test void appendsDmJdbcUrlParameters() throws Exception { String url = invokeBuildUrl(new ConnectParams( diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 317bc47906..cdced5ed0a 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -50,6 +50,7 @@ import { mongodbAuthFailureHint, mongoConnectionUsesOidc, mongoUrlParam, mongoUr import { isMongoLegacyDriverProfile } from "@/lib/mongo/mongoCapabilities"; import { mysqlCleartextPasswordAuthEnabled, setMysqlCleartextPasswordAuthEnabled } from "@/lib/database/mysqlConnectionOptions"; import { applyDamengSslUrlParams, damengSslFormConfig } from "@/lib/database/damengSslOptions"; +import { DAMENG_BUILTIN_DRIVER_PROFILE, DAMENG_CUSTOM_DRIVER_PROFILE, DAMENG_DEFAULT_JDBC_DRIVER_CLASS, damengCustomJdbcUrl, damengDriverModeForConfig, defaultDamengJdbcUrl, type DamengDriverMode } from "@/lib/database/damengDriverOptions"; import { doltSystemTablesVisible, isDoltDriverProfile, setDoltSystemTablesVisible } from "@/lib/database/doltProfile"; import { DamengJvmSystemPropertyError, damengJvmSystemPropertiesText, parseDamengJvmSystemProperties } from "@/lib/database/damengJvmOptions"; import { copyToClipboard } from "@/lib/common/clipboard"; @@ -2315,6 +2316,13 @@ function applyProfile(val: string, preserveConnectionFields = false) { form.value.port = 0; form.value.connection_string = undefined; } + if (profile.type === "dameng") { + form.value.connection_string = undefined; + form.value.jdbc_driver_class = undefined; + form.value.jdbc_driver_paths = []; + jdbcDriverPathsInput.value = ""; + jdbcManualClasspathOpen.value = false; + } if (profile.type === "jdbc") { form.value.host = ""; form.value.connection_string = ""; @@ -2752,6 +2760,29 @@ function switchH2DriverProfile(profile: "h2" | "h2-v1" | "h2-v2" | "h2-v3" | "h2 resetTestState(); } +const damengDriverMode = computed(() => damengDriverModeForConfig(form.value)); +const isDamengCustomDriver = computed(() => form.value.db_type === "dameng" && damengDriverMode.value === "custom"); + +function setDamengDriverMode(mode: DamengDriverMode) { + if (mode === "builtin") { + form.value.driver_profile = DAMENG_BUILTIN_DRIVER_PROFILE; + form.value.connection_string = undefined; + form.value.jdbc_driver_class = undefined; + form.value.jdbc_driver_paths = []; + jdbcDriverPathsInput.value = ""; + selectedJdbcDriverPath.value = ""; + jdbcManualClasspathOpen.value = false; + } else { + form.value.driver_profile = DAMENG_CUSTOM_DRIVER_PROFILE; + // Do not materialize connection_string here: submit regenerates it from + // the live form, and freezing the URL at switch time would keep the + // connection on the old host/port after later edits. + form.value.jdbc_driver_class = form.value.jdbc_driver_class?.trim() || DAMENG_DEFAULT_JDBC_DRIVER_CLASS; + jdbcManualClasspathOpen.value = true; + } + resetTestState(); +} + function isH2FileJdbcUrlLikePath(value: string): boolean { return /\.(mv|h2)\.db$/i.test(value.trim()) || value.includes("/") || value.includes("\\"); } @@ -2856,7 +2887,7 @@ function dbCategoryForOption(value: string): DbCategoryKey | undefined { const selectedDbIcon = computed(() => iconTypeMap[selectedType.value] || selectedProfile().icon || selectedType.value); function supportsNativeAgentJdbcDriverConfigType(dbType: DatabaseType): boolean { - return dbType === "prestosql" || dbType === "bigquery"; + return dbType === "prestosql" || dbType === "bigquery" || dbType === "dameng"; } const jdbcBackedDatabaseTypes = new Set(["jdbc", "prestosql", "bigquery"]); @@ -2871,7 +2902,7 @@ const jdbcxHighPrivilegeExtensionsAllowed = computed({ resetTestState(); }, }); -const supportsNativeAgentJdbcDriverConfig = computed(() => supportsNativeAgentJdbcDriverConfigType(form.value.db_type)); +const supportsNativeAgentJdbcDriverConfig = computed(() => supportsNativeAgentJdbcDriverConfigType(form.value.db_type) && (form.value.db_type !== "dameng" || isDamengCustomDriver.value)); const isH2FileMode = computed(() => form.value.db_type === "h2" && h2ConnectionMode.value === "file"); const isH2CustomDriver = computed(() => form.value.db_type === "h2" && form.value.driver_profile === "h2-custom"); const usesLocalFilePathInput = computed(() => isLocalFileTypeDb(form.value.db_type) && (form.value.db_type !== "h2" || isH2FileMode.value)); @@ -3941,6 +3972,22 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo } else { config.ca_cert_path = config.ca_cert_path?.trim() || ""; } + if (config.db_type === "dameng") { + if (damengDriverModeForConfig(config) === "custom") { + config.driver_profile = DAMENG_CUSTOM_DRIVER_PROFILE; + config.connection_string = damengCustomJdbcUrl(config); + config.jdbc_driver_class = config.jdbc_driver_class?.trim() || DAMENG_DEFAULT_JDBC_DRIVER_CLASS; + config.jdbc_driver_paths = parsedJdbcDriverPaths(); + if (config.jdbc_driver_paths.length === 0) { + throw new Error(t("connection.damengCustomDriverRequired")); + } + } else { + config.driver_profile = DAMENG_BUILTIN_DRIVER_PROFILE; + config.connection_string = undefined; + config.jdbc_driver_class = undefined; + config.jdbc_driver_paths = []; + } + } if (jdbcBackedDatabaseTypes.has(config.db_type) || gaussdbConnectionMode(config) === "m-jdbc") { if (config.db_type === "jdbc") { if (config.driver_profile === "dremio") { @@ -7131,6 +7178,26 @@ function openExternalUrl(url: string) { +
+ +
+ + + + + + + + {{ t("connection.damengDriverModeHint") }} + + +
+
+