From 7e6923d53b2a81a9b26ed7aae7751c31d802a944 Mon Sep 17 00:00:00 2001 From: zipg Date: Tue, 4 Aug 2026 09:55:36 +0800 Subject: [PATCH] fix(gbase8s): stabilize connections and table access Co-authored-by: zipg <4047349+zipg@users.noreply.github.com> --- .../java/com/dbx/agent/AbstractJdbcAgent.java | 6 +- .../dbx/agent/MultiSessionJsonRpcServer.java | 4 +- .../com/dbx/agent/gbase8s/Gbase8sAgent.java | 86 +++++++++------- .../dbx/agent/gbase8s/Gbase8sAgentTest.java | 98 +++++++++++++++++++ .../connection/ConnectionDialog.vue | 14 +-- .../connection/ConnectionErrorIndicator.vue | 10 +- .../src/i18n/__tests__/backendErrors.spec.ts | 5 + apps/desktop/src/i18n/backend-errors.ts | 1 + apps/desktop/src/i18n/locales/en.ts | 2 + apps/desktop/src/i18n/locales/es.ts | 2 + apps/desktop/src/i18n/locales/it.ts | 2 + apps/desktop/src/i18n/locales/ja.ts | 2 + apps/desktop/src/i18n/locales/ko.ts | 2 + apps/desktop/src/i18n/locales/pt-BR.ts | 2 + apps/desktop/src/i18n/locales/zh-CN.ts | 2 + apps/desktop/src/i18n/locales/zh-TW.ts | 2 + .../__tests__/database/jdbcDialect.spec.ts | 8 ++ .../__tests__/table/tableSelectSql.spec.ts | 11 +++ .../__tests__/dialogTextSelection.spec.ts | 49 ++++++++++ .../src/lib/connection/dialogTextSelection.ts | 11 +++ apps/desktop/src/lib/database/jdbcDialect.ts | 4 + apps/desktop/src/lib/table/tableSelectSql.ts | 7 +- .../connectionStore.metadataLoading.spec.ts | 45 +++++++++ crates/dbx-core/src/data_grid_sql.rs | 34 +++++++ .../dbx-core/src/sql_dialect/table_select.rs | 1 + crates/dbx-core/src/sql_dialect/tests.rs | 11 +++ 26 files changed, 375 insertions(+), 46 deletions(-) create mode 100644 apps/desktop/src/lib/connection/__tests__/dialogTextSelection.spec.ts create mode 100644 apps/desktop/src/lib/connection/dialogTextSelection.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 67a0945b1b..dc2ffea5ee 100644 --- a/agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java +++ b/agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java @@ -270,6 +270,10 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent { poolRegistry = registry; } + public boolean supportsConnectionPooling() { + return true; + } + final synchronized boolean usesConnectionPool() { return poolRegistry != null; } @@ -531,7 +535,7 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent { return this::resultValue; } - private Connection openInitializedConnection(ConnectParams params) throws Exception { + protected final Connection openInitializedConnection(ConnectParams params) throws Exception { Connection opened = openConnection(params); try { afterPhysicalConnect(params, opened); diff --git a/agents/common/src/main/java/com/dbx/agent/MultiSessionJsonRpcServer.java b/agents/common/src/main/java/com/dbx/agent/MultiSessionJsonRpcServer.java index 4d4f9bf59a..feaef812c7 100644 --- a/agents/common/src/main/java/com/dbx/agent/MultiSessionJsonRpcServer.java +++ b/agents/common/src/main/java/com/dbx/agent/MultiSessionJsonRpcServer.java @@ -181,7 +181,9 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable { session = new Session(sessionHandlerFactory.get()); } else { DatabaseAgent agent = agentFactory.get(); - if (poolRegistry.isEnabled() && agent instanceof AbstractJdbcAgent jdbcAgent) { + if (poolRegistry.isEnabled() + && agent instanceof AbstractJdbcAgent jdbcAgent + && jdbcAgent.supportsConnectionPooling()) { jdbcAgent.attachConnectionPoolRegistry(poolRegistry); ensureMaintenanceStarted(); } diff --git a/agents/drivers/gbase8s/src/main/java/com/dbx/agent/gbase8s/Gbase8sAgent.java b/agents/drivers/gbase8s/src/main/java/com/dbx/agent/gbase8s/Gbase8sAgent.java index e547406e55..c3dff220d2 100644 --- a/agents/drivers/gbase8s/src/main/java/com/dbx/agent/gbase8s/Gbase8sAgent.java +++ b/agents/drivers/gbase8s/src/main/java/com/dbx/agent/gbase8s/Gbase8sAgent.java @@ -46,11 +46,17 @@ public final class Gbase8sAgent extends ConfiguredJdbcAgent { private String tableCacheSchema = ""; private long tableCacheTimeMillis; private List tableCache = Collections.emptyList(); + private ConnectParams databaseListParams; public Gbase8sAgent() { super(GBASE8S_PROFILE); } + @Override + public boolean supportsConnectionPooling() { + return false; + } + public static String buildUrl(ConnectParams params) { if (!params.getConnection_string().trim().isEmpty()) { return params.getConnection_string(); @@ -78,6 +84,38 @@ public final class Gbase8sAgent extends ConfiguredJdbcAgent { return defaultGbaseServer(params.getHost()); } + static String buildUrlForDatabase(ConnectParams params, String database) { + return buildUrl(paramsForDatabase(params, database)); + } + + private static ConnectParams paramsForDatabase(ConnectParams params, String database) { + String connectionString = trim(params.getConnection_string()); + if (!connectionString.isEmpty()) { + int schemeEnd = connectionString.indexOf("://"); + int databaseStart = schemeEnd < 0 ? -1 : connectionString.indexOf('/', schemeEnd + 3); + if (databaseStart >= 0) { + int paramsStart = connectionString.indexOf(':', databaseStart + 1); + String suffix = paramsStart >= 0 ? connectionString.substring(paramsStart) : ""; + connectionString = connectionString.substring(0, databaseStart + 1) + database + suffix; + } + } + + ConnectParams databaseParams = new ConnectParams( + params.getHost(), + params.getPort(), + database, + params.getUsername(), + params.getPassword(), + params.getUrl_params(), + connectionString, + params.isMysql_compat_mode(), + params.getJdbc_driver_class(), + params.getJdbc_driver_paths() + ); + databaseParams.setGbase_server(getGbaseServer(params)); + return databaseParams; + } + @Override protected String buildJdbcUrl(ConnectParams params) { return buildUrl(params); @@ -86,11 +124,13 @@ public final class Gbase8sAgent extends ConfiguredJdbcAgent { @Override protected void afterConnect(ConnectParams params, Connection connection) { super.afterConnect(params, connection); + databaseListParams = paramsForDatabase(params, "sysmaster"); clearMetadataCache(); } @Override protected void afterDisconnect() { + databaseListParams = null; clearMetadataCache(); } @@ -109,13 +149,7 @@ public final class Gbase8sAgent extends ConfiguredJdbcAgent { if (cached != null) { return cached; } - List names = queryDatabaseNamesInCatalog("sysmaster", "SELECT name FROM sysdatabases ORDER BY name"); - if (names.isEmpty()) { - names = queryDatabaseNames("SELECT name FROM sysmaster:sysdatabases ORDER BY name"); - } - if (names.isEmpty()) { - names = queryDatabaseNames("SELECT name FROM sysdatabases ORDER BY name"); - } + List names = queryDatabaseNamesFromSysmaster(); if (names.isEmpty()) { return super.listDatabases(); } @@ -135,8 +169,14 @@ public final class Gbase8sAgent extends ConfiguredJdbcAgent { if (cached != null) { return cached; } + Connection connection = requireConnection(); + if (!connection.getMetaData().supportsSchemasInDataManipulation()) { + List schemas = Collections.emptyList(); + cacheSchemas(catalog, schemas); + return schemas; + } Set schemas = new LinkedHashSet<>(); - try (PreparedStatement stmt = requireConnection().prepareStatement( + try (PreparedStatement stmt = connection.prepareStatement( "SELECT DISTINCT owner FROM systables WHERE tabid >= 100 AND tabtype IN ('T', 'V') ORDER BY owner" ); ResultSet rs = stmt.executeQuery()) { while (rs.next()) { @@ -419,33 +459,13 @@ public final class Gbase8sAgent extends ConfiguredJdbcAgent { return value.toLowerCase(Locale.ROOT).contains(needle.toLowerCase(Locale.ROOT)); } - private List queryDatabaseNames(String sql) { - try { - return queryDatabaseNames(requireConnection(), sql); - } catch (Exception ignored) { + private List queryDatabaseNamesFromSysmaster() { + ConnectParams params = databaseListParams; + if (params == null) { return Collections.emptyList(); } - } - - private List queryDatabaseNamesInCatalog(String catalog, String sql) { - try { - Connection connection = requireConnection(); - String previousCatalog = ""; - try { - previousCatalog = trim(connection.getCatalog()); - } catch (Exception ignored) { - } - connection.setCatalog(catalog); - try { - return queryDatabaseNames(connection, sql); - } finally { - if (!previousCatalog.isEmpty()) { - try { - connection.setCatalog(previousCatalog); - } catch (Exception ignored) { - } - } - } + try (Connection connection = openInitializedConnection(params)) { + return queryDatabaseNames(connection, "SELECT name FROM sysdatabases ORDER BY name"); } catch (Exception ignored) { return Collections.emptyList(); } diff --git a/agents/drivers/gbase8s/src/test/java/com/dbx/agent/gbase8s/Gbase8sAgentTest.java b/agents/drivers/gbase8s/src/test/java/com/dbx/agent/gbase8s/Gbase8sAgentTest.java index 2b3e6d7333..b848af12ee 100644 --- a/agents/drivers/gbase8s/src/test/java/com/dbx/agent/gbase8s/Gbase8sAgentTest.java +++ b/agents/drivers/gbase8s/src/test/java/com/dbx/agent/gbase8s/Gbase8sAgentTest.java @@ -14,6 +14,7 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; @@ -31,6 +32,7 @@ class Gbase8sAgentTest { Assertions.assertEquals("jdbc:gbasedbt-sqli://{host}:{port}/{database}:GBASEDBTSERVER=gbase8s", agent.getProfile().getUrlTemplate()); Assertions.assertEquals(9088, agent.getProfile().getDefaultPort()); Assertions.assertTrue(agent.getProfile().getSkipExecutionContext()); + Assertions.assertFalse(agent.supportsConnectionPooling()); } @Test @@ -114,6 +116,71 @@ class Gbase8sAgentTest { Assertions.assertEquals("jdbc:gbasedbt-sqli://db.example.com:20013/app:GBASEDBTSERVER=gbase01", url); } + @Test + void buildsSysmasterUrlWithoutChangingTheConfiguredDatabase() { + ConnectParams params = new ConnectParams( + "db.example.com", + 20013, + "appdb", + "user", + "password", + "CLIENT_LOCALE=zh_cn.utf8", + "", + false + ); + params.setGbase_server("gbase01"); + + Assertions.assertEquals( + "jdbc:gbasedbt-sqli://db.example.com:20013/sysmaster:GBASEDBTSERVER=gbase01;CLIENT_LOCALE=zh_cn.utf8", + Gbase8sAgent.buildUrlForDatabase(params, "sysmaster") + ); + } + + @Test + void replacesDatabaseInCustomConnectionStringForDatabaseListing() { + ConnectParams params = new ConnectParams( + "", + 0, + "", + "user", + "password", + "", + "jdbc:gbasedbt-sqli://db.example.com:20013/appdb:GBASEDBTSERVER=gbase01;CLIENT_LOCALE=zh_cn.utf8", + false + ); + + Assertions.assertEquals( + "jdbc:gbasedbt-sqli://db.example.com:20013/sysmaster:GBASEDBTSERVER=gbase01;CLIENT_LOCALE=zh_cn.utf8", + Gbase8sAgent.buildUrlForDatabase(params, "sysmaster") + ); + } + + @Test + void omitsOwnerSchemasWhenTheDatabaseCannotUseThemInDml() { + List sql = new ArrayList<>(); + Gbase8sAgent agent = new Gbase8sAgent(); + TestSupport.setPrivateConnection( + agent, + schemaConnection(false, sql, resultSet(new String[]{"owner"}, new Object[][]{{"gbasedbt"}})) + ); + + Assertions.assertTrue(agent.listSchemas().isEmpty()); + Assertions.assertTrue(sql.isEmpty()); + } + + @Test + void listsOwnerSchemasWhenTheDatabaseSupportsThemInDml() { + List sql = new ArrayList<>(); + Gbase8sAgent agent = new Gbase8sAgent(); + TestSupport.setPrivateConnection( + agent, + schemaConnection(true, sql, resultSet(new String[]{"owner"}, new Object[][]{{"gbasedbt"}})) + ); + + Assertions.assertEquals(List.of("gbasedbt"), agent.listSchemas()); + Assertions.assertEquals(1, sql.size()); + } + @Test void constrainedListTablesUsesGbase8sSystemTableQuery() { List sql = new ArrayList<>(); @@ -367,6 +434,37 @@ class Gbase8sAgentTest { }); } + private static Connection schemaConnection(boolean supportsSchemasInDml, List sql, ResultSet resultSet) { + DatabaseMetaData metadata = proxy(DatabaseMetaData.class, (method, args) -> { + if ("supportsSchemasInDataManipulation".equals(method.getName())) { + return supportsSchemasInDml; + } + return defaultValue(method.getReturnType()); + }); + PreparedStatement statement = proxy(PreparedStatement.class, (method, args) -> { + if ("executeQuery".equals(method.getName())) { + return resultSet; + } + return defaultValue(method.getReturnType()); + }); + return proxy(Connection.class, (method, args) -> { + if ("getCatalog".equals(method.getName())) { + return "appdb"; + } + if ("getMetaData".equals(method.getName())) { + return metadata; + } + if ("prepareStatement".equals(method.getName())) { + sql.add(String.valueOf(args[0])); + return statement; + } + if ("isClosed".equals(method.getName())) { + return false; + } + return defaultValue(method.getReturnType()); + }); + } + private static ResultSet resultSet(String[] columns, Object[][] rows) { int[] index = {-1}; Object[] lastValue = {null}; diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index f372b777fd..2b0b6fdc99 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -51,6 +51,7 @@ import { JDBCX_DEFAULT_URL, JDBCX_DRIVER_PROFILE, JDBCX_JDBC_DRIVER_CLASS, ensur import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection"; import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout"; import { appendConnectionErrorHints, isJdbcMissingRuntimeDependencyError } from "@/lib/connection/connectionErrorHints"; +import { preventDialogDocumentSelectAll } from "@/lib/connection/dialogTextSelection"; import { postgresTlsModeForForm } from "@/lib/connection/postgresTlsMode"; import { buildMqKafkaConnectionExtra, mqKafkaConnectionTarget, resolveMqKafkaConnectionSource, type MqKafkaConnectionSource } from "@/lib/connection/mqKafkaConnection"; import { assertCompleteDatabaseCategories, databaseSelectionForCategory } from "@/lib/connection/databaseCategoryOptions"; @@ -4735,7 +4736,7 @@ function openExternalUrl(url: string) {