From a9c14c1cbcb0ddc5ceec6e54b0490760d858dbd4 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Sun, 18 Oct 2026 09:15:34 +0200 Subject: [PATCH 1/7] dbeaver/pro#11166 Add JDBC data source nullability annotations (#42455) * dbeaver/pro#11166 Add JDBC data source nullability annotations * Fix tests logging * dbeaver/pro#11166 Guard uninitialized default JDBC instances * dbeaver/pro#11166 Update test mocks for default instance availability * dbeaver/pro#11166 Align data source annotations and code style * dbeaver/pro#11166 Finish data source code style fixes --- .../altibase/model/AltibaseDataSource.java | 9 +- .../ext/athena/model/AthenaDataSource.java | 4 +- .../ext/bigquery/model/BQDataSource.java | 6 +- .../model/ClickhouseDataSource.java | 13 +- .../ext/cubrid/model/CubridDataSource.java | 9 +- .../ext/dameng/model/DamengDataSource.java | 14 +- .../databend/model/DatabendDataSource.java | 10 +- .../ext/databricks/DatabricksDataSource.java | 5 +- .../ext/db2/i/model/DB2IDataSource.java | 4 +- .../ext/db2/zos/model/DB2ZOSDataSource.java | 5 +- .../dbeaver/ext/db2/model/DB2DataSource.java | 111 +++++----- .../ext/doris/model/DorisDataSource.java | 3 +- .../ext/duckdb/model/DuckDBDataSource.java | 8 +- .../ext/exasol/model/ExasolDataSource.java | 193 +++++++++--------- .../ext/gbase8s/model/GBase8sDataSource.java | 9 +- .../ext/generic/model/GenericDataSource.java | 8 +- .../ext/hana/model/HANADataSource.java | 9 +- .../ext/hive/model/HiveDataSource.java | 3 +- .../ext/iotdb/model/IoTDBDataSource.java | 2 +- .../ext/mssql/model/SQLServerDataSource.java | 26 ++- .../generic/SQLServerGenericDataSource.java | 7 +- .../ext/mysql/model/MySQLDataSource.java | 21 +- .../ext/netezza/model/NetezzaDataSource.java | 31 ++- .../ext/oracle/model/OracleDataSource.java | 155 +++++++------- .../postgresql/model/PostgreDataSource.java | 22 +- .../snowflake/model/SnowflakeDataSource.java | 7 +- .../ext/spanner/model/SpannerDataSource.java | 20 +- .../ext/sqlite/model/SQLiteDataSource.java | 8 +- .../starrocks/model/StarRocksDataSource.java | 5 +- .../tidb/mysql/model/TiDBMySQLDataSource.java | 8 +- .../tidblake/model/TiDBLakeDataSource.java | 10 +- .../ext/vertica/model/VerticaDataSource.java | 2 +- .../model/impl/jdbc/JDBCDataSource.java | 114 +++++++---- .../sql/completion/SQLCompletionAnalyzer.java | 8 +- .../SQLQueryConnectionDummyContext.java | 7 +- .../src/org/jkiss/dbeaver/model/DBUtils.java | 2 +- .../model/struct/DBSInstanceContainer.java | 7 +- .../registry/DataSourceDescriptor.java | 18 +- .../registry/DataSourceProviderRegistry.java | 4 +- .../control/DashboardListViewer.java | 4 +- .../sql/preferences/PrefPageSQLFormat.java | 2 +- .../actions/NavigatorHandlerLinkEditor.java | 6 +- .../generic/test/GenericBaseTableDDLTest.java | 9 + .../ext/generic/test/VerticaSSLUtilsTest.java | 3 +- .../greenplum/model/GreenplumTableTest.java | 3 +- .../ext/mimer/model/MimerStatementTest.java | 1 + .../model/MimerUtilsExternalRoutineTest.java | 1 + .../oracle/model/OracleUserProfileTest.java | 1 + 48 files changed, 554 insertions(+), 383 deletions(-) diff --git a/plugins/org.jkiss.dbeaver.ext.altibase/src/org/jkiss/dbeaver/ext/altibase/model/AltibaseDataSource.java b/plugins/org.jkiss.dbeaver.ext.altibase/src/org/jkiss/dbeaver/ext/altibase/model/AltibaseDataSource.java index 0674224c544a2..b7bea34c4f470 100644 --- a/plugins/org.jkiss.dbeaver.ext.altibase/src/org/jkiss/dbeaver/ext/altibase/model/AltibaseDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.altibase/src/org/jkiss/dbeaver/ext/altibase/model/AltibaseDataSource.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,9 +108,10 @@ public void initialize(@NotNull DBRProgressMonitor monitor) throws DBException { @Override protected void initializeContextState( - @NotNull DBRProgressMonitor monitor, - @NotNull JDBCExecutionContext context, - JDBCExecutionContext initFrom) throws DBException { + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { super.initializeContextState(monitor, context, initFrom); diff --git a/plugins/org.jkiss.dbeaver.ext.athena/src/org/jkiss/dbeaver/ext/athena/model/AthenaDataSource.java b/plugins/org.jkiss.dbeaver.ext.athena/src/org/jkiss/dbeaver/ext/athena/model/AthenaDataSource.java index cb14cdeeb0922..5454443da5b4c 100644 --- a/plugins/org.jkiss.dbeaver.ext.athena/src/org/jkiss/dbeaver/ext/athena/model/AthenaDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.athena/src/org/jkiss/dbeaver/ext/athena/model/AthenaDataSource.java @@ -17,6 +17,7 @@ package org.jkiss.dbeaver.ext.athena.model; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.ModelPreferences; import org.jkiss.dbeaver.ext.generic.model.GenericDataSource; @@ -43,11 +44,12 @@ public AthenaDataSource(DBRProgressMonitor monitor, DBPDataSourceContainer conta super(monitor, container, metaModel, new AthenaSQLDialect()); } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { diff --git a/plugins/org.jkiss.dbeaver.ext.bigquery/src/org/jkiss/dbeaver/ext/bigquery/model/BQDataSource.java b/plugins/org.jkiss.dbeaver.ext.bigquery/src/org/jkiss/dbeaver/ext/bigquery/model/BQDataSource.java index fc54b0f6c5342..b8e547ebfcdcb 100644 --- a/plugins/org.jkiss.dbeaver.ext.bigquery/src/org/jkiss/dbeaver/ext/bigquery/model/BQDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.bigquery/src/org/jkiss/dbeaver/ext/bigquery/model/BQDataSource.java @@ -17,6 +17,7 @@ package org.jkiss.dbeaver.ext.bigquery.model; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.ext.generic.model.GenericDataSource; import org.jkiss.dbeaver.ext.generic.model.meta.GenericMetaModel; @@ -43,8 +44,9 @@ public BQDataSource( super(monitor, container, metaModel, new BQSQLDialect()); } + @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) throws DBCException { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) throws DBCException { return new BQExecutionContext(instance, type); } @@ -66,7 +68,7 @@ protected String getConnectionURL(@NotNull DBPConnectionConfiguration connection protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { diff --git a/plugins/org.jkiss.dbeaver.ext.clickhouse/src/org/jkiss/dbeaver/ext/clickhouse/model/ClickhouseDataSource.java b/plugins/org.jkiss.dbeaver.ext.clickhouse/src/org/jkiss/dbeaver/ext/clickhouse/model/ClickhouseDataSource.java index e4346ac1b74d7..6470857b9f193 100644 --- a/plugins/org.jkiss.dbeaver.ext.clickhouse/src/org/jkiss/dbeaver/ext/clickhouse/model/ClickhouseDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.clickhouse/src/org/jkiss/dbeaver/ext/clickhouse/model/ClickhouseDataSource.java @@ -100,8 +100,12 @@ ClickhouseTableEngine getEngineByName(@NotNull String engineName) { @NotNull @Override - protected Properties getAllConnectionProperties(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, @NotNull - String purpose, @NotNull DBPConnectionConfiguration connectionInfo) throws DBCException { + protected Properties getAllConnectionProperties( + @NotNull DBRProgressMonitor monitor, + @Nullable JDBCExecutionContext context, + @NotNull String purpose, + @NotNull DBPConnectionConfiguration connectionInfo + ) throws DBCException { Properties properties = super.getAllConnectionProperties(monitor, context, purpose, connectionInfo); if (!CommonUtils.toBoolean(properties.getProperty(ClickhouseConstants.PROP_USE_SERVER_TIME_ZONE)) && !CommonUtils.toBoolean(properties.getProperty(ClickhouseConstants.PROP_USE_TIME_ZONE)) @@ -185,7 +189,7 @@ private void configureSession(@NotNull Properties properties) { // Canceling @Override - public void cancelStatementExecute(DBRProgressMonitor monitor, JDBCStatement statement) throws DBException { + public void cancelStatementExecute(@NotNull DBRProgressMonitor monitor, @NotNull JDBCStatement statement) throws DBException { try { super.cancelStatementExecute(monitor, statement); @@ -273,8 +277,9 @@ public String getDefaultDataTypeName(@NotNull DBPDataKind dataKind) { } } + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { return new ClickhouseDataSourceInfo(metaData); } diff --git a/plugins/org.jkiss.dbeaver.ext.cubrid/src/org/jkiss/dbeaver/ext/cubrid/model/CubridDataSource.java b/plugins/org.jkiss.dbeaver.ext.cubrid/src/org/jkiss/dbeaver/ext/cubrid/model/CubridDataSource.java index e85ab490e4c8b..158e3d533d4f1 100644 --- a/plugins/org.jkiss.dbeaver.ext.cubrid/src/org/jkiss/dbeaver/ext/cubrid/model/CubridDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.cubrid/src/org/jkiss/dbeaver/ext/cubrid/model/CubridDataSource.java @@ -317,10 +317,11 @@ public DBSObject refreshObject(@NotNull DBRProgressMonitor monitor) throws DBExc } @Override - protected void initializeContextState(DBRProgressMonitor monitor, - JDBCExecutionContext context, - JDBCExecutionContext initFrom) - throws DBException { + protected void initializeContextState( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { super.initializeContextState(monitor, context, initFrom); if (outputReader == null && checkSupportDbmsOutput(monitor, context)) { diff --git a/plugins/org.jkiss.dbeaver.ext.dameng/src/org/jkiss/dbeaver/ext/dameng/model/DamengDataSource.java b/plugins/org.jkiss.dbeaver.ext.dameng/src/org/jkiss/dbeaver/ext/dameng/model/DamengDataSource.java index 06faca10e50d2..e7179b4b1fb01 100644 --- a/plugins/org.jkiss.dbeaver.ext.dameng/src/org/jkiss/dbeaver/ext/dameng/model/DamengDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.dameng/src/org/jkiss/dbeaver/ext/dameng/model/DamengDataSource.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,13 +57,18 @@ public DamengDataSource(DBRProgressMonitor monitor, DBPDataSourceContainer conta super(monitor, container, metaModel, new DamengSQLDialect()); } + @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new DamengExecutionContext(instance, type); } @Override - protected void initializeContextState(DBRProgressMonitor monitor, JDBCExecutionContext context, JDBCExecutionContext initFrom) throws DBException { + protected void initializeContextState( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { DamengExecutionContext executionContext = (DamengExecutionContext) context; if (initFrom == null) { executionContext.refreshDefaults(monitor, true); @@ -76,8 +81,9 @@ protected void initializeContextState(DBRProgressMonitor monitor, JDBCExecutionC } } + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, JDBCDatabaseMetaData metaData) { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { return new JDBCDataSourceInfo(metaData); } diff --git a/plugins/org.jkiss.dbeaver.ext.databend/src/org/jkiss/dbeaver/ext/databend/model/DatabendDataSource.java b/plugins/org.jkiss.dbeaver.ext.databend/src/org/jkiss/dbeaver/ext/databend/model/DatabendDataSource.java index 37a41df6b5e74..25ffdbec99d16 100644 --- a/plugins/org.jkiss.dbeaver.ext.databend/src/org/jkiss/dbeaver/ext/databend/model/DatabendDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.databend/src/org/jkiss/dbeaver/ext/databend/model/DatabendDataSource.java @@ -17,6 +17,7 @@ package org.jkiss.dbeaver.ext.databend.model; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.ext.generic.model.GenericDataSource; import org.jkiss.dbeaver.model.DBPDataSourceContainer; @@ -46,9 +47,12 @@ protected boolean isPopulateClientAppName() { @NotNull @Override - protected Properties getAllConnectionProperties(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, @NotNull - String purpose, @NotNull DBPConnectionConfiguration connectionInfo) - throws DBCException { + protected Properties getAllConnectionProperties( + @NotNull DBRProgressMonitor monitor, + @Nullable JDBCExecutionContext context, + @NotNull String purpose, + @NotNull DBPConnectionConfiguration connectionInfo + ) throws DBCException { Properties properties = super.getAllConnectionProperties(monitor, context, purpose, connectionInfo); final DBWHandlerConfiguration sslConfig = getContainer().getActualConnectionConfiguration().getHandler("databend-ssl"); diff --git a/plugins/org.jkiss.dbeaver.ext.databricks/src/org/jkiss/dbeaver/ext/databricks/DatabricksDataSource.java b/plugins/org.jkiss.dbeaver.ext.databricks/src/org/jkiss/dbeaver/ext/databricks/DatabricksDataSource.java index 2292ffed8b213..cfb2d505bed5d 100644 --- a/plugins/org.jkiss.dbeaver.ext.databricks/src/org/jkiss/dbeaver/ext/databricks/DatabricksDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.databricks/src/org/jkiss/dbeaver/ext/databricks/DatabricksDataSource.java @@ -77,7 +77,7 @@ public boolean isLegacyDriver() { @Override protected Properties getAllConnectionProperties( @NotNull DBRProgressMonitor monitor, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { @@ -111,8 +111,9 @@ protected void initializeContextState( } } + @NotNull @Override - public JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + public JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new DatabricksExecutionContext(instance, type); } diff --git a/plugins/org.jkiss.dbeaver.ext.db2.i/src/org/jkiss/dbeaver/ext/db2/i/model/DB2IDataSource.java b/plugins/org.jkiss.dbeaver.ext.db2.i/src/org/jkiss/dbeaver/ext/db2/i/model/DB2IDataSource.java index 2c933fed7628b..d234b3b87981e 100644 --- a/plugins/org.jkiss.dbeaver.ext.db2.i/src/org/jkiss/dbeaver/ext/db2/i/model/DB2IDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.db2.i/src/org/jkiss/dbeaver/ext/db2/i/model/DB2IDataSource.java @@ -17,6 +17,7 @@ package org.jkiss.dbeaver.ext.db2.i.model; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.ModelPreferences; @@ -46,11 +47,12 @@ public DB2IDataSource(DBRProgressMonitor monitor, DBPDataSourceContainer contain super(monitor, container, metaModel, new DB2ISQLDialect()); } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) { diff --git a/plugins/org.jkiss.dbeaver.ext.db2.zos/src/org/jkiss/dbeaver/ext/db2/zos/model/DB2ZOSDataSource.java b/plugins/org.jkiss.dbeaver.ext.db2.zos/src/org/jkiss/dbeaver/ext/db2/zos/model/DB2ZOSDataSource.java index 9889f9beba620..129263ca5bb00 100644 --- a/plugins/org.jkiss.dbeaver.ext.db2.zos/src/org/jkiss/dbeaver/ext/db2/zos/model/DB2ZOSDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.db2.zos/src/org/jkiss/dbeaver/ext/db2/zos/model/DB2ZOSDataSource.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,11 +46,12 @@ public DB2ZOSDataSource(DBRProgressMonitor monitor, DBPDataSourceContainer conta super(monitor, container, metaModel, new DB2ZOSSQLDialect()); } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) { diff --git a/plugins/org.jkiss.dbeaver.ext.db2/src/org/jkiss/dbeaver/ext/db2/model/DB2DataSource.java b/plugins/org.jkiss.dbeaver.ext.db2/src/org/jkiss/dbeaver/ext/db2/model/DB2DataSource.java index c24cb973fbb55..bda884cd40919 100644 --- a/plugins/org.jkiss.dbeaver.ext.db2/src/org/jkiss/dbeaver/ext/db2/model/DB2DataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.db2/src/org/jkiss/dbeaver/ext/db2/model/DB2DataSource.java @@ -36,8 +36,8 @@ import org.jkiss.dbeaver.ext.db2.model.security.DB2GranteeCache; import org.jkiss.dbeaver.ext.db2.model.security.DB2Role; import org.jkiss.dbeaver.model.*; -import org.jkiss.dbeaver.model.access.DBAPasswordChangeInfo; -import org.jkiss.dbeaver.model.access.DBAuthUtils; +import org.jkiss.dbeaver.model.access.DBAPasswordChangeInfo; +import org.jkiss.dbeaver.model.access.DBAuthUtils; import org.jkiss.dbeaver.model.admin.sessions.DBAServerSessionManager; import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; import org.jkiss.dbeaver.model.connection.DBPDriver; @@ -191,18 +191,21 @@ public void initialize(@NotNull DBRProgressMonitor monitor) throws DBException } } - @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) throws DBCException { + @NotNull + @Override + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) throws DBCException { return new DB2ExecutionContext(instance, type); } - protected void initializeContextState(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, - JDBCExecutionContext initFrom) throws DBException - { + protected void initializeContextState( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { if (initFrom != null) { - ((DB2ExecutionContext)context).setCurrentSchema(monitor, ((DB2ExecutionContext)initFrom).getDefaultSchema()); + ((DB2ExecutionContext) context).setCurrentSchema(monitor, ((DB2ExecutionContext) initFrom).getDefaultSchema()); } else { - ((DB2ExecutionContext)context).refreshDefaults(monitor, true); + ((DB2ExecutionContext) context).refreshDefaults(monitor, true); } } @@ -228,9 +231,9 @@ public void cacheStructure(@NotNull DBRProgressMonitor monitor, int scope) throw // Connection related Info // ----------------------- + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) - { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { final DB2DataSourceInfo info = new DB2DataSourceInfo(metaData); // Compute Database version @@ -266,11 +269,12 @@ protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @No return info; } - @Override - protected Map getInternalConnectionProperties( + @NotNull + @Override + protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { @@ -285,15 +289,16 @@ protected Map getInternalConnectionProperties( return props; } + @NotNull + @Override + public ErrorType discoverErrorType(@NotNull Throwable error) { + if (isPasswordExpired(error)) { + return ErrorType.PASSWORD_EXPIRED; + } + return super.discoverErrorType(error); + } + @NotNull - @Override - public ErrorType discoverErrorType(@NotNull Throwable error) { - if (isPasswordExpired(error)) { - return ErrorType.PASSWORD_EXPIRED; - } - return super.discoverErrorType(error); - } - @Override protected Connection openConnection( @NotNull DBRProgressMonitor monitor, @@ -304,8 +309,8 @@ protected Connection openConnection( try { db2Connection = super.openConnection(monitor, context, purpose); } catch (DBCException e) { - if ((!isBigSQL() && !isWarehouse()) && isPasswordExpired(e) && changeExpiredPassword(monitor)) { - return openConnection(monitor, context, purpose); + if ((!isBigSQL() && !isWarehouse()) && isPasswordExpired(e) && changeExpiredPassword(monitor)) { + return openConnection(monitor, context, purpose); } throw e; } @@ -325,19 +330,19 @@ protected Connection openConnection( return db2Connection; } - private boolean isPasswordExpired(@NotNull Throwable error) { - return JDBCUtils.matchesSQLException(error, this::isPasswordExpired); - } - - private boolean isPasswordExpired(@NotNull SQLException sqlEx) { - if (sqlEx.getErrorCode() != DB2Constants.ER_MUST_CHANGE_PASSWORD_LOGIN || - !DB2Constants.ER_STATE_MUST_CHANGE_PASSWORD_LOGIN.equals(sqlEx.getSQLState())) { + private boolean isPasswordExpired(@NotNull Throwable error) { + return JDBCUtils.matchesSQLException(error, this::isPasswordExpired); + } + + private boolean isPasswordExpired(@NotNull SQLException sqlEx) { + if (sqlEx.getErrorCode() != DB2Constants.ER_MUST_CHANGE_PASSWORD_LOGIN || + !DB2Constants.ER_STATE_MUST_CHANGE_PASSWORD_LOGIN.equals(sqlEx.getSQLState())) { return false; } try { - Object errorSrc = BeanUtils.invokeObjectDeclaredMethod( - sqlEx, + Object errorSrc = BeanUtils.invokeObjectDeclaredMethod( + sqlEx, "getErrorSrc", new Class[0], new Object[0] @@ -347,26 +352,26 @@ private boolean isPasswordExpired(@NotNull SQLException sqlEx) { log.error("Failed to retrieve DB2 error source from SQLException", ex); return false; } - } - - private boolean changeExpiredPassword(@NotNull DBRProgressMonitor monitor) throws DBCException { - DBAPasswordChangeInfo passwordChangeInfo = DBAuthUtils.getPendingPasswordChange( - container.getActualConnectionConfiguration()); - if (passwordChangeInfo == null) { - return DBAuthUtils.promptAndChangePasswordForCurrentUser(monitor, container, this::changeUserPassword); - } - try { - DBAuthUtils.changePasswordForCurrentUser( - monitor, - container, - this::changeUserPassword, - passwordChangeInfo - ); - return true; - } catch (DBException e) { - throw new DBCException("Error changing expired password", e); - } - } + } + + private boolean changeExpiredPassword(@NotNull DBRProgressMonitor monitor) throws DBCException { + DBAPasswordChangeInfo passwordChangeInfo = DBAuthUtils.getPendingPasswordChange( + container.getActualConnectionConfiguration()); + if (passwordChangeInfo == null) { + return DBAuthUtils.promptAndChangePasswordForCurrentUser(monitor, container, this::changeUserPassword); + } + try { + DBAuthUtils.changePasswordForCurrentUser( + monitor, + container, + this::changeUserPassword, + passwordChangeInfo + ); + return true; + } catch (DBException e) { + throw new DBCException("Error changing expired password", e); + } + } private void changeUserPassword( @NotNull DBRProgressMonitor monitor, diff --git a/plugins/org.jkiss.dbeaver.ext.doris/src/org/jkiss/dbeaver/ext/doris/model/DorisDataSource.java b/plugins/org.jkiss.dbeaver.ext.doris/src/org/jkiss/dbeaver/ext/doris/model/DorisDataSource.java index 79dde79db005e..b8036f3cf5ba0 100644 --- a/plugins/org.jkiss.dbeaver.ext.doris/src/org/jkiss/dbeaver/ext/doris/model/DorisDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.doris/src/org/jkiss/dbeaver/ext/doris/model/DorisDataSource.java @@ -70,8 +70,9 @@ public DorisDataSource( super(monitor, container, metaModel, new DorisDialect()); } + @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new DorisExecutionContext(instance, type); } diff --git a/plugins/org.jkiss.dbeaver.ext.duckdb/src/org/jkiss/dbeaver/ext/duckdb/model/DuckDBDataSource.java b/plugins/org.jkiss.dbeaver.ext.duckdb/src/org/jkiss/dbeaver/ext/duckdb/model/DuckDBDataSource.java index c19280d632950..9632086c1bf28 100644 --- a/plugins/org.jkiss.dbeaver.ext.duckdb/src/org/jkiss/dbeaver/ext/duckdb/model/DuckDBDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.duckdb/src/org/jkiss/dbeaver/ext/duckdb/model/DuckDBDataSource.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,13 +52,15 @@ public DBPDataKind resolveDataKind(@NotNull String typeName, int valueType) { } } + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { return new DuckDBDataSourceInfo(getContainer().getDriver(), metaData); } + @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new DuckDBExecutionContext(instance, type); } } diff --git a/plugins/org.jkiss.dbeaver.ext.exasol/src/org/jkiss/dbeaver/ext/exasol/model/ExasolDataSource.java b/plugins/org.jkiss.dbeaver.ext.exasol/src/org/jkiss/dbeaver/ext/exasol/model/ExasolDataSource.java index e089d89575f8d..ac28fa10695ec 100644 --- a/plugins/org.jkiss.dbeaver.ext.exasol/src/org/jkiss/dbeaver/ext/exasol/model/ExasolDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.exasol/src/org/jkiss/dbeaver/ext/exasol/model/ExasolDataSource.java @@ -356,19 +356,20 @@ int getDriverMajorVersion() @NotNull @Override - protected Properties getAllConnectionProperties(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, @NotNull - String purpose, - @NotNull DBPConnectionConfiguration connectionInfo) throws DBCException { - - Properties props = super.getAllConnectionProperties(monitor, context, purpose, connectionInfo); - - if (addMetaProps == null) - addMetaProps = new Properties(); - - addMetaProps.clear(); - - return props; - + protected Properties getAllConnectionProperties( + @NotNull DBRProgressMonitor monitor, + @Nullable JDBCExecutionContext context, + @NotNull String purpose, + @NotNull DBPConnectionConfiguration connectionInfo + ) throws DBCException { + Properties props = super.getAllConnectionProperties(monitor, context, purpose, connectionInfo); + + if (addMetaProps == null) { + addMetaProps = new Properties(); + } + addMetaProps.clear(); + + return props; } @Nullable @@ -398,96 +399,100 @@ public ErrorPosition[] getErrorPosition(@NotNull DBRProgressMonitor monitor, @No return null; } - @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { - return new ExasolExecutionContext(instance, type); - } + @NotNull + @Override + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { + return new ExasolExecutionContext(instance, type); + } - protected void initializeContextState(@NotNull DBRProgressMonitor monitor, - @NotNull JDBCExecutionContext context, JDBCExecutionContext initFrom) - throws DBException - { - if (initFrom != null) { - ((ExasolExecutionContext)context).setCurrentSchema(monitor, ((ExasolExecutionContext)initFrom).getDefaultSchema()); - } else { - ((ExasolExecutionContext)context).refreshDefaults(monitor, true); - } - } + protected void initializeContextState( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { + if (initFrom != null) { + ((ExasolExecutionContext) context).setCurrentSchema(monitor, ((ExasolExecutionContext) initFrom).getDefaultSchema()); + } else { + ((ExasolExecutionContext) context).refreshDefaults(monitor, true); + } + } - @Override - public T getAdapter(@NotNull Class adapter) - { - if (adapter == DBSStructureAssistant.class) { - return adapter.cast(new ExasolStructureAssistant(this)); - } else if (adapter == DBAServerSessionManager.class) { - return adapter.cast(new ExasolServerSessionManager(this)); - } else if (adapter == DBAUserPasswordManager.class) { - return adapter.cast(new ExasolChangeUserPasswordManager(this)); - } else if (adapter == DBCQueryPlanner.class) { - return adapter.cast(new ExasolQueryPlanner(this)); - } - return super.getAdapter(adapter); - } + @Nullable + @Override + public T getAdapter(@NotNull Class adapter) { + if (adapter == DBSStructureAssistant.class) { + return adapter.cast(new ExasolStructureAssistant(this)); + } else if (adapter == DBAServerSessionManager.class) { + return adapter.cast(new ExasolServerSessionManager(this)); + } else if (adapter == DBAUserPasswordManager.class) { + return adapter.cast(new ExasolChangeUserPasswordManager(this)); + } else if (adapter == DBCQueryPlanner.class) { + return adapter.cast(new ExasolQueryPlanner(this)); + } + return super.getAdapter(adapter); + } - @Override - public void cacheStructure(@NotNull DBRProgressMonitor monitor, int scope) - throws DBException - { - // TODO DF: No idea what to do with this method, what it is used for... - } + @Override + public void cacheStructure(@NotNull DBRProgressMonitor monitor, int scope) throws DBException { + // TODO DF: No idea what to do with this method, what it is used for... + } - // ----------------------- - // Connection related Info - // ----------------------- + // ----------------------- + // Connection related Info + // ----------------------- - @Override - protected DBPDataSourceInfo createDataSourceInfo( - DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) - { - final ExasolDataSourceInfo info = new ExasolDataSourceInfo(metaData); + @NotNull + @Override + protected DBPDataSourceInfo createDataSourceInfo( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCDatabaseMetaData metaData + ) { + final ExasolDataSourceInfo info = new ExasolDataSourceInfo(metaData); - info.setSupportsResultSetScroll(false); + info.setSupportsResultSetScroll(false); - return info; - } + return info; + } - @Override - protected Map getInternalConnectionProperties( - @NotNull DBRProgressMonitor monitor, - @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, - @NotNull String purpose, - @NotNull DBPConnectionConfiguration connectionInfo - ) throws DBCException { - Map props = new HashMap<>(ExasolDataSourceProvider.getConnectionsProps()); - if (CommonUtils.getBoolean(connectionInfo.getProviderProperty(ExasolConstants.DRV_USE_LEGACY_ENCRYPTION), false)) { - props.put(ExasolConstants.DRV_LEGACY_ENCRYPTION, "1"); - } - return props; - } + @NotNull + @Override + protected Map getInternalConnectionProperties( + @NotNull DBRProgressMonitor monitor, + @NotNull DBPDriver driver, + @Nullable JDBCExecutionContext context, + @NotNull String purpose, + @NotNull DBPConnectionConfiguration connectionInfo + ) throws DBCException { + Map props = new HashMap<>(ExasolDataSourceProvider.getConnectionsProps()); + if (CommonUtils.getBoolean(connectionInfo.getProviderProperty(ExasolConstants.DRV_USE_LEGACY_ENCRYPTION), false)) { + props.put(ExasolConstants.DRV_LEGACY_ENCRYPTION, "1"); + } + return props; + } - @Override - public DBSObject refreshObject(@NotNull DBRProgressMonitor monitor) throws DBException { - super.refreshObject(monitor); - - this.schemaCache.clearCache(); - this.connectionCache.clearCache(); - this.userCache.clearCache(); - //caches for security - if (this.roleCache != null) { - this.roleCache.clearCache(); - } - if (this.baseTableGrantCache != null) { - this.baseTableGrantCache.clearCache(); - } - if (this.systemGrantCache != null) { - this.systemGrantCache.clearCache(); - } - if (this.connectionGrantCache != null) { - this.connectionGrantCache.clearCache(); - } - return this; - } + @NotNull + @Override + public DBSObject refreshObject(@NotNull DBRProgressMonitor monitor) throws DBException { + super.refreshObject(monitor); + + this.schemaCache.clearCache(); + this.connectionCache.clearCache(); + this.userCache.clearCache(); + // Caches for security + if (this.roleCache != null) { + this.roleCache.clearCache(); + } + if (this.baseTableGrantCache != null) { + this.baseTableGrantCache.clearCache(); + } + if (this.systemGrantCache != null) { + this.systemGrantCache.clearCache(); + } + if (this.connectionGrantCache != null) { + this.connectionGrantCache.clearCache(); + } + return this; + } // -------------------------- // Manage Children: ExasolSchema diff --git a/plugins/org.jkiss.dbeaver.ext.gbase8s/src/org/jkiss/dbeaver/ext/gbase8s/model/GBase8sDataSource.java b/plugins/org.jkiss.dbeaver.ext.gbase8s/src/org/jkiss/dbeaver/ext/gbase8s/model/GBase8sDataSource.java index 88645e6691bc2..854e4499f98d5 100644 --- a/plugins/org.jkiss.dbeaver.ext.gbase8s/src/org/jkiss/dbeaver/ext/gbase8s/model/GBase8sDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.gbase8s/src/org/jkiss/dbeaver/ext/gbase8s/model/GBase8sDataSource.java @@ -17,6 +17,7 @@ package org.jkiss.dbeaver.ext.gbase8s.model; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.ext.gbase8s.GBase8sUtils; @@ -76,8 +77,9 @@ public GBase8sDataSource(DBRProgressMonitor monitor, DBPDataSourceContainer cont super(monitor, container, metaModel, new GBase8sDialect()); } + @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { // replace the connector of the database/table in queryGetActiveDB replaceConnector4GetActiveDB(container); return new GenericExecutionContext(instance, type); @@ -102,8 +104,9 @@ void replaceConnector4GetActiveDB(DBPDataSourceContainer container) { } } + @NotNull @Override - public ErrorType discoverErrorType(Throwable error) { + public ErrorType discoverErrorType(@NotNull Throwable error) { // #41319 when calling PreparedStatement.setNCharacterStream() // the driver should be thrown SQLFeatureNotSupportedException, but it doesn't // so detect it by analyzing errorCode and handle it properly @@ -124,7 +127,7 @@ public ErrorType discoverErrorType(Throwable error) { * @param connectProps {@inheritDoc} */ @Override - protected void fillConnectionProperties(DBPConnectionConfiguration connectionInfo, Properties connectProps) { + protected void fillConnectionProperties(@NotNull DBPConnectionConfiguration connectionInfo, @NotNull Properties connectProps) { super.fillConnectionProperties(connectionInfo, connectProps); // handle JDBCTEMP driver properties (#41669), use existed value first, then fallback to default value diff --git a/plugins/org.jkiss.dbeaver.ext.generic/src/org/jkiss/dbeaver/ext/generic/model/GenericDataSource.java b/plugins/org.jkiss.dbeaver.ext.generic/src/org/jkiss/dbeaver/ext/generic/model/GenericDataSource.java index fc985f814eaa2..be077c83e3f74 100644 --- a/plugins/org.jkiss.dbeaver.ext.generic/src/org/jkiss/dbeaver/ext/generic/model/GenericDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.generic/src/org/jkiss/dbeaver/ext/generic/model/GenericDataSource.java @@ -210,15 +210,16 @@ protected boolean isPopulateClientAppName() { return getClass() != GenericDataSource.class; } + @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) throws DBCException { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) throws DBCException { return new GenericExecutionContext(instance, type); } protected void initializeContextState( @NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, - JDBCExecutionContext initFrom + @Nullable JDBCExecutionContext initFrom ) throws DBException { super.initializeContextState(monitor, context, initFrom); if (initFrom != null) { @@ -243,9 +244,10 @@ public GenericMetaObject getMetaObject(String id) { return metaModel.getMetaObject(id); } + @NotNull @Override protected DBPDataSourceInfo createDataSourceInfo( - DBRProgressMonitor monitor, + @NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData ) { return new GenericDataSourceInfo(container.getDriver(), metaData); diff --git a/plugins/org.jkiss.dbeaver.ext.hana/src/org/jkiss/dbeaver/ext/hana/model/HANADataSource.java b/plugins/org.jkiss.dbeaver.ext.hana/src/org/jkiss/dbeaver/ext/hana/model/HANADataSource.java index d765ede50af18..0a3967c43e431 100644 --- a/plugins/org.jkiss.dbeaver.ext.hana/src/org/jkiss/dbeaver/ext/hana/model/HANADataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.hana/src/org/jkiss/dbeaver/ext/hana/model/HANADataSource.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,9 +74,9 @@ public HANADataSource(DBRProgressMonitor monitor, DBPDataSourceContainer contain super(monitor, container, metaModel, new HANASQLDialect()); } + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) - { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { final HANADataSourceInfo info = new HANADataSourceInfo(metaData); return info; } @@ -131,11 +131,12 @@ protected boolean isPopulateClientAppName() { return false; // basically true, but different property name } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { diff --git a/plugins/org.jkiss.dbeaver.ext.hive/src/org/jkiss/dbeaver/ext/hive/model/HiveDataSource.java b/plugins/org.jkiss.dbeaver.ext.hive/src/org/jkiss/dbeaver/ext/hive/model/HiveDataSource.java index b039368fb73f1..80b8c9e2561be 100644 --- a/plugins/org.jkiss.dbeaver.ext.hive/src/org/jkiss/dbeaver/ext/hive/model/HiveDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.hive/src/org/jkiss/dbeaver/ext/hive/model/HiveDataSource.java @@ -46,8 +46,9 @@ public HiveDataSource(DBRProgressMonitor monitor, DBPDataSourceContainer contain super(monitor, container, metaModel, new HiveSQLDialect()); } + @NotNull @Override - protected HiveDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { + protected HiveDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { return new HiveDataSourceInfo(monitor, this, metaData); } diff --git a/plugins/org.jkiss.dbeaver.ext.iotdb/src/org/jkiss/dbeaver/ext/iotdb/model/IoTDBDataSource.java b/plugins/org.jkiss.dbeaver.ext.iotdb/src/org/jkiss/dbeaver/ext/iotdb/model/IoTDBDataSource.java index 620530c75c47d..243f864495614 100644 --- a/plugins/org.jkiss.dbeaver.ext.iotdb/src/org/jkiss/dbeaver/ext/iotdb/model/IoTDBDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.iotdb/src/org/jkiss/dbeaver/ext/iotdb/model/IoTDBDataSource.java @@ -56,7 +56,7 @@ public IoTDBDataSource(DBRProgressMonitor monitor, @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new IoTDBExecutionContext(instance, type); } diff --git a/plugins/org.jkiss.dbeaver.ext.mssql/src/org/jkiss/dbeaver/ext/mssql/model/SQLServerDataSource.java b/plugins/org.jkiss.dbeaver.ext.mssql/src/org/jkiss/dbeaver/ext/mssql/model/SQLServerDataSource.java index 0487602831148..1211026fd17ab 100644 --- a/plugins/org.jkiss.dbeaver.ext.mssql/src/org/jkiss/dbeaver/ext/mssql/model/SQLServerDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.mssql/src/org/jkiss/dbeaver/ext/mssql/model/SQLServerDataSource.java @@ -133,9 +133,9 @@ public boolean supportsExternalTables(JDBCSession session) { return supportsIsExternalColumn; } + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) - { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { SQLServerDataSourceInfo info = new SQLServerDataSourceInfo(this, metaData); if (isDataWarehouseServer(monitor)) { info.setSupportsResultSetScroll(false); @@ -218,12 +218,17 @@ public ServerLoginCache getServerLoginCache() { @NotNull @Override - protected Properties getAllConnectionProperties(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, @NotNull - String purpose, @NotNull DBPConnectionConfiguration connectionInfo) throws DBCException { + protected Properties getAllConnectionProperties( + @NotNull DBRProgressMonitor monitor, + @Nullable JDBCExecutionContext context, + @NotNull String purpose, + @NotNull DBPConnectionConfiguration connectionInfo + ) throws DBCException { Properties properties = super.getAllConnectionProperties(monitor, context, purpose, connectionInfo); - if (!getContainer().getPreferenceStore().getBoolean(ModelPreferences.META_CLIENT_NAME_DISABLE)) {// App name + if (!getContainer().getPreferenceStore().getBoolean(ModelPreferences.META_CLIENT_NAME_DISABLE)) { // App name properties.put( - SQLServerUtils.isDriverJtds(getContainer().getDriver()) ? SQLServerConstants.APPNAME_CLIENT_PROPERTY : SQLServerConstants.APPLICATION_NAME_CLIENT_PROPERTY, + SQLServerUtils.isDriverJtds(getContainer().getDriver()) + ? SQLServerConstants.APPNAME_CLIENT_PROPERTY : SQLServerConstants.APPLICATION_NAME_CLIENT_PROPERTY, CommonUtils.truncateString(DBUtils.getClientApplicationName(getContainer(), context, purpose), 64)); } @@ -368,13 +373,18 @@ private boolean updateExpiredPassword() { return true; } + @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new SQLServerExecutionContext(instance, type); } @Override - protected void initializeContextState(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, JDBCExecutionContext initFrom) throws DBException { + protected void initializeContextState( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { super.initializeContextState(monitor, context, initFrom); if (initFrom != null) { if (!isDataWarehouseServer(monitor)) { diff --git a/plugins/org.jkiss.dbeaver.ext.mssql/src/org/jkiss/dbeaver/ext/mssql/model/generic/SQLServerGenericDataSource.java b/plugins/org.jkiss.dbeaver.ext.mssql/src/org/jkiss/dbeaver/ext/mssql/model/generic/SQLServerGenericDataSource.java index 12e087b412e30..8cae0b2411bc9 100644 --- a/plugins/org.jkiss.dbeaver.ext.mssql/src/org/jkiss/dbeaver/ext/mssql/model/generic/SQLServerGenericDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.mssql/src/org/jkiss/dbeaver/ext/mssql/model/generic/SQLServerGenericDataSource.java @@ -17,6 +17,7 @@ package org.jkiss.dbeaver.ext.mssql.model.generic; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.ModelPreferences; @@ -66,14 +67,16 @@ public SQLServerGenericDataSource(DBRProgressMonitor monitor, DBPDataSourceConta public SQLServerGenericDataSource(DBRProgressMonitor monitor, DBPDataSourceContainer container, SQLServerMetaModel metaModel) throws DBException { - super(monitor, container, metaModel, metaModel.getServerType() == ServerType.SQL_SERVER ? new SQLServerDialectMssql() : new SQLServerDialectSybase()); + super(monitor, container, metaModel, + metaModel.getServerType() == ServerType.SQL_SERVER ? new SQLServerDialectMssql() : new SQLServerDialectSybase()); } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { diff --git a/plugins/org.jkiss.dbeaver.ext.mysql/src/org/jkiss/dbeaver/ext/mysql/model/MySQLDataSource.java b/plugins/org.jkiss.dbeaver.ext.mysql/src/org/jkiss/dbeaver/ext/mysql/model/MySQLDataSource.java index 95053d062676d..5d441ed39ef07 100644 --- a/plugins/org.jkiss.dbeaver.ext.mysql/src/org/jkiss/dbeaver/ext/mysql/model/MySQLDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.mysql/src/org/jkiss/dbeaver/ext/mysql/model/MySQLDataSource.java @@ -145,11 +145,12 @@ int getLowerCaseTableNames() { return lowerCaseTableNames; } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { @@ -196,8 +197,9 @@ protected Map getInternalConnectionProperties( return props; } + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { return new MySQLDataSourceInfo(metaData); } @@ -253,19 +255,24 @@ private String makeKeyStorePath(Path keyStorePath) throws MalformedURLException } } + @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new MySQLExecutionContext(instance, type); } - protected void initializeContextState(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, JDBCExecutionContext initFrom) throws DBException { + protected void initializeContextState( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { if (initFrom != null && !context.getDataSource().getContainer().isConnectionReadOnly()) { - MySQLCatalog object = ((MySQLExecutionContext)initFrom).getDefaultCatalog(); + MySQLCatalog object = ((MySQLExecutionContext) initFrom).getDefaultCatalog(); if (object != null) { - ((MySQLExecutionContext)context).setCurrentDatabase(monitor, object); + ((MySQLExecutionContext) context).setCurrentDatabase(monitor, object); } } else { - ((MySQLExecutionContext)context).refreshDefaults(monitor, true); + ((MySQLExecutionContext) context).refreshDefaults(monitor, true); } } diff --git a/plugins/org.jkiss.dbeaver.ext.netezza/src/org/jkiss/dbeaver/ext/netezza/model/NetezzaDataSource.java b/plugins/org.jkiss.dbeaver.ext.netezza/src/org/jkiss/dbeaver/ext/netezza/model/NetezzaDataSource.java index d47d73b39e79f..e435d8ab241a1 100644 --- a/plugins/org.jkiss.dbeaver.ext.netezza/src/org/jkiss/dbeaver/ext/netezza/model/NetezzaDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.netezza/src/org/jkiss/dbeaver/ext/netezza/model/NetezzaDataSource.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,21 +36,30 @@ public class NetezzaDataSource extends GenericDataSource { private static final Log log = Log.getLog(NetezzaDataSource.class); private static final String COMMAND_SYSTEM_PROPERTY = "sun.java.command"; - public NetezzaDataSource(@NotNull DBRProgressMonitor monitor, @NotNull DBPDataSourceContainer container, - @NotNull GenericMetaModel metaModel) throws DBException { + public NetezzaDataSource( + @NotNull DBRProgressMonitor monitor, + @NotNull DBPDataSourceContainer container, + @NotNull GenericMetaModel metaModel + ) throws DBException { super(monitor, container, metaModel, new NetezzaSQLDialect()); } // Constructor for tests @ForTest - public NetezzaDataSource(@NotNull DBRProgressMonitor monitor, @NotNull GenericMetaModel metaModel, - @NotNull DBPDataSourceContainer container) throws DBException { + public NetezzaDataSource( + @NotNull DBRProgressMonitor monitor, + @NotNull GenericMetaModel metaModel, + @NotNull DBPDataSourceContainer container + ) throws DBException { super(monitor, metaModel, container, new NetezzaSQLDialect()); } @Override - protected void initializeContextState(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, - JDBCExecutionContext initFrom) throws DBException { + protected void initializeContextState( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { super.initializeContextState(monitor, context, initFrom); final String name = getContainer().getActualConnectionConfiguration().getUserName(); if (getSQLDialect().isQuotedIdentifier(name)) { @@ -66,9 +75,13 @@ protected void initializeContextState(@NotNull DBRProgressMonitor monitor, @NotN } } + @NotNull @Override - protected Connection openConnection(@NotNull DBRProgressMonitor monitor, @Nullable JDBCExecutionContext context, - @NotNull String purpose) throws DBCException { + protected Connection openConnection( + @NotNull DBRProgressMonitor monitor, + @Nullable JDBCExecutionContext context, + @NotNull String purpose + ) throws DBCException { // Workaround for an issue where older Netezza JDBC drivers get a NullPointerException // when system property "sun.java.command" is null. // See https://github.com/baztian/jaydebeapi/issues/103 diff --git a/plugins/org.jkiss.dbeaver.ext.oracle/src/org/jkiss/dbeaver/ext/oracle/model/OracleDataSource.java b/plugins/org.jkiss.dbeaver.ext.oracle/src/org/jkiss/dbeaver/ext/oracle/model/OracleDataSource.java index 3a17e266b7242..c9f6d07e728b9 100644 --- a/plugins/org.jkiss.dbeaver.ext.oracle/src/org/jkiss/dbeaver/ext/oracle/model/OracleDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.oracle/src/org/jkiss/dbeaver/ext/oracle/model/OracleDataSource.java @@ -29,8 +29,8 @@ import org.jkiss.dbeaver.ext.oracle.model.session.OracleServerSessionManager; import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.access.DBAPasswordChangeInfo; -import org.jkiss.dbeaver.model.access.DBAuthUtils; import org.jkiss.dbeaver.model.access.DBAUserPasswordManager; +import org.jkiss.dbeaver.model.access.DBAuthUtils; import org.jkiss.dbeaver.model.admin.sessions.DBAServerSessionManager; import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; import org.jkiss.dbeaver.model.connection.DBPDriver; @@ -222,7 +222,7 @@ protected Connection openConnection(@NotNull DBRProgressMonitor monitor, @Nullab } return connection; } catch (DBCException e) { - if (JDBCUtils.matchesSQLException(e, exception -> exception.getErrorCode() == OracleConstants.EC_PASSWORD_EXPIRED)) { + if (JDBCUtils.matchesSQLException(e, exception -> exception.getErrorCode() == OracleConstants.EC_PASSWORD_EXPIRED)) { // Here we could try to ask for expired password change // This is supported for thin driver since Oracle 12.2 if (changeExpiredPassword(monitor, context, purpose)) { @@ -245,21 +245,31 @@ private boolean checkForPasswordWillExpireWarning(@NotNull SQLWarning warning) { return false; } - private boolean changeExpiredPassword(DBRProgressMonitor monitor, JDBCExecutionContext context, String purpose) throws DBCException { - // Ref: https://stackoverflow.com/questions/21733300/oracle-password-expiry-and-grace-period-handling-using-java-oracle-jdbc - - DBPConnectionConfiguration connectionInfo = getContainer().getActualConnectionConfiguration(); - DBAPasswordChangeInfo passwordInfo = DBAuthUtils.getPendingPasswordChange(connectionInfo); - boolean interactive = passwordInfo == null; - if (interactive) { - if (DBWorkbench.getPlatform().getApplication().isHeadlessMode()) { - return false; - } - passwordInfo = DBWorkbench.getPlatformUI().promptUserPasswordChange("Password has expired. Set new password.", connectionInfo.getUserName(), connectionInfo.getUserPassword(), true, true); - } - if (passwordInfo == null) { - return false; - } + private boolean changeExpiredPassword( + @NotNull DBRProgressMonitor monitor, + @Nullable JDBCExecutionContext context, + @NotNull String purpose + ) throws DBCException { + // Ref: https://stackoverflow.com/questions/21733300/oracle-password-expiry-and-grace-period-handling-using-java-oracle-jdbc + + DBPConnectionConfiguration connectionInfo = getContainer().getActualConnectionConfiguration(); + DBAPasswordChangeInfo passwordInfo = DBAuthUtils.getPendingPasswordChange(connectionInfo); + boolean interactive = passwordInfo == null; + if (interactive) { + if (DBWorkbench.getPlatform().getApplication().isHeadlessMode()) { + return false; + } + passwordInfo = DBWorkbench.getPlatformUI().promptUserPasswordChange( + "Password has expired. Set new password.", + connectionInfo.getUserName(), + connectionInfo.getUserPassword(), + true, + true + ); + } + if (passwordInfo == null) { + return false; + } // Obtain connection try { @@ -274,35 +284,39 @@ private boolean changeExpiredPassword(DBRProgressMonitor monitor, JDBCExecutionC final String url = getConnectionURL(connectionInfo); monitor.subTask("Connecting for expired password change"); Driver driverInstance = getDriverInstance(monitor); - try (Connection connection = driverInstance.connect(url, connectProps)) { - if (connection == null) { - throw new DBCException("Null connection returned"); - } - } - - connectionInfo.setUserPassword(passwordInfo.getNewPassword()); - getContainer().getConnectionConfiguration().setUserPassword(passwordInfo.getNewPassword()); - DBAuthUtils.clearPendingPasswordChange(connectionInfo); - if (interactive) { - getContainer().persistConfiguration(); - } - return true; - } - catch (Exception e) { - if (!interactive) { - throw new DBCException("Error changing expired password", e); - } - DBWorkbench.getPlatformUI().showError("Error changing password", "Error changing expired password", e); - return false; - } + try (Connection connection = driverInstance.connect(url, connectProps)) { + if (connection == null) { + throw new DBCException("Null connection returned"); + } + } + + connectionInfo.setUserPassword(passwordInfo.getNewPassword()); + getContainer().getConnectionConfiguration().setUserPassword(passwordInfo.getNewPassword()); + DBAuthUtils.clearPendingPasswordChange(connectionInfo); + if (interactive) { + getContainer().persistConfiguration(); + } + return true; + } catch (Exception e) { + if (!interactive) { + throw new DBCException("Error changing expired password", e); + } + DBWorkbench.getPlatformUI().showError("Error changing password", "Error changing expired password", e); + return false; + } } - @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + @NotNull + @Override + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new OracleExecutionContext(instance, type); } - protected void initializeContextState(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, JDBCExecutionContext initFrom) throws DBException { + protected void initializeContextState( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { if (outputReader == null) { outputReader = new OracleOutputReader(); } @@ -400,35 +414,37 @@ public OracleSchema getDefaultSchema() { return (OracleSchema) DBUtils.getDefaultContext(this, true).getContextDefaults().getDefaultSchema(); } + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { return new OracleDataSourceInfo(metaData); } + @NotNull + @Override + public ErrorType discoverErrorType(@NotNull Throwable error) { + if (JDBCUtils.matchesSQLException(error, exception -> exception.getErrorCode() == OracleConstants.EC_PASSWORD_EXPIRED)) { + return ErrorType.PASSWORD_EXPIRED; + } + if (JDBCUtils.matchesSQLException(error, exception -> exception.getErrorCode() == OracleConstants.EC_INVALID_USERNAME_PASSWORD + || exception.getErrorCode() == OracleConstants.EC_INVALID_OLD_PASSWORD)) { + return ErrorType.AUTHENTICATION_FAILED; + } + if (JDBCUtils.matchesSQLException(error, exception -> exception.getErrorCode() == OracleConstants.EC_NO_RESULTSET_AVAILABLE)) { + return ErrorType.RESULT_SET_MISSING; + } + if (JDBCUtils.matchesSQLException(error, exception -> exception.getErrorCode() == OracleConstants.EC_FEATURE_NOT_SUPPORTED)) { + return ErrorType.FEATURE_UNSUPPORTED; + } + return super.discoverErrorType(error); + } + @NotNull @Override - public ErrorType discoverErrorType(@NotNull Throwable error) { - if (JDBCUtils.matchesSQLException(error, exception -> exception.getErrorCode() == OracleConstants.EC_PASSWORD_EXPIRED)) { - return ErrorType.PASSWORD_EXPIRED; - } - if (JDBCUtils.matchesSQLException(error, exception -> exception.getErrorCode() == OracleConstants.EC_INVALID_USERNAME_PASSWORD - || exception.getErrorCode() == OracleConstants.EC_INVALID_OLD_PASSWORD)) { - return ErrorType.AUTHENTICATION_FAILED; - } - if (JDBCUtils.matchesSQLException(error, exception -> exception.getErrorCode() == OracleConstants.EC_NO_RESULTSET_AVAILABLE)) { - return ErrorType.RESULT_SET_MISSING; - } - if (JDBCUtils.matchesSQLException(error, exception -> exception.getErrorCode() == OracleConstants.EC_FEATURE_NOT_SUPPORTED)) { - return ErrorType.FEATURE_UNSUPPORTED; - } - return super.discoverErrorType(error); - } - - @Override - protected Map getInternalConnectionProperties( + protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { @@ -497,13 +513,14 @@ public OracleUser getUser(DBRProgressMonitor monitor, String name) throws DBExce return userCache.getObject(monitor, this, name); } + @NotNull + public JDBCStructCache getProfileCache() { + return profileCache; + } + @NotNull - public JDBCStructCache getProfileCache() { - return profileCache; - } - - @Association - public Collection getProfiles(DBRProgressMonitor monitor) throws DBException { + @Association + public Collection getProfiles(@NotNull DBRProgressMonitor monitor) throws DBException { return profileCache.getAllObjects(monitor, this); } @@ -664,7 +681,7 @@ public T getAdapter(@NotNull Class adapter) { } @Override - public void cancelStatementExecute(DBRProgressMonitor monitor, JDBCStatement statement) throws DBException { + public void cancelStatementExecute(@NotNull DBRProgressMonitor monitor, @NotNull JDBCStatement statement) throws DBException { if (driverSupportsQueryCancel()) { super.cancelStatementExecute(monitor, statement); } else { diff --git a/plugins/org.jkiss.dbeaver.ext.postgresql/src/org/jkiss/dbeaver/ext/postgresql/model/PostgreDataSource.java b/plugins/org.jkiss.dbeaver.ext.postgresql/src/org/jkiss/dbeaver/ext/postgresql/model/PostgreDataSource.java index 940763b8651a6..667c2d8e21df1 100644 --- a/plugins/org.jkiss.dbeaver.ext.postgresql/src/org/jkiss/dbeaver/ext/postgresql/model/PostgreDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.postgresql/src/org/jkiss/dbeaver/ext/postgresql/model/PostgreDataSource.java @@ -269,11 +269,12 @@ public PostgreDatabase createDatabaseImpl(DBRProgressMonitor monitor, String nam return new PostgreDatabase(monitor, this, name, owner, templateName, tablespace, encoding); } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { @@ -380,13 +381,17 @@ private void initProxySSL(Map props, DBWHandlerConfiguration ssl //initServerSSL(props, sslConfig); } + @NotNull @Override - protected PostgreExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + protected PostgreExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new PostgreExecutionContext((PostgreDatabase) instance, type); } - protected void initializeContextState(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, - @Nullable JDBCExecutionContext initFrom) throws DBException { + protected void initializeContextState( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { PostgreExecutionContext postgreContext = (PostgreExecutionContext) context; PostgreSchema activeSchema = null; if (initFrom != null) { @@ -705,6 +710,11 @@ public String getDefaultDataTypeName(@NotNull DBPDataKind dataKind) { return getDefaultInstance().getDefaultDataTypeName(dataKind); } + @Override + public boolean hasDefaultInstance() { + return databaseCache != null && !databaseCache.getCachedObjects().isEmpty(); + } + @NotNull @Override public PostgreDatabase getDefaultInstance() { @@ -950,9 +960,9 @@ public ErrorType discoverErrorType(@NotNull Throwable error) { return super.discoverErrorType(error); } + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) - { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { return new PostgreDataSourceInfo(this, metaData); } diff --git a/plugins/org.jkiss.dbeaver.ext.snowflake/src/org/jkiss/dbeaver/ext/snowflake/model/SnowflakeDataSource.java b/plugins/org.jkiss.dbeaver.ext.snowflake/src/org/jkiss/dbeaver/ext/snowflake/model/SnowflakeDataSource.java index 1928ad43f5ce9..8611d156746db 100644 --- a/plugins/org.jkiss.dbeaver.ext.snowflake/src/org/jkiss/dbeaver/ext/snowflake/model/SnowflakeDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.snowflake/src/org/jkiss/dbeaver/ext/snowflake/model/SnowflakeDataSource.java @@ -58,11 +58,12 @@ public SnowflakeDataSource( super(monitor, container, metaModel, dialect); } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) { @@ -120,7 +121,7 @@ private static boolean isPasswordAuthenticator(@Nullable String authenticator) { @Override protected Properties getAllConnectionProperties( @NotNull DBRProgressMonitor monitor, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { @@ -133,7 +134,7 @@ protected Properties getAllConnectionProperties( @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new SnowflakeExecutionContext(instance, type); } diff --git a/plugins/org.jkiss.dbeaver.ext.spanner/src/org/jkiss/dbeaver/ext/spanner/model/SpannerDataSource.java b/plugins/org.jkiss.dbeaver.ext.spanner/src/org/jkiss/dbeaver/ext/spanner/model/SpannerDataSource.java index a3c4afd686a0a..a1e9a43dee9c0 100644 --- a/plugins/org.jkiss.dbeaver.ext.spanner/src/org/jkiss/dbeaver/ext/spanner/model/SpannerDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.spanner/src/org/jkiss/dbeaver/ext/spanner/model/SpannerDataSource.java @@ -45,21 +45,23 @@ public SpannerDataSource(DBRProgressMonitor monitor, DBPDataSourceContainer cont super(monitor, container, metaModel, new SpannerSQLDialect()); } + @Nullable @Override - protected String getConnectionURL(@NotNull DBPConnectionConfiguration connectionInfo) throws DBException { - String url = super.getConnectionURL(connectionInfo); - if (url != null && url.startsWith("jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s")) { - // Official driver. - return String.format(url, connectionInfo.getServerName(), connectionInfo.getHostName(), connectionInfo.getDatabaseName()); - } - return url; - } + protected String getConnectionURL(@NotNull DBPConnectionConfiguration connectionInfo) throws DBException { + String url = super.getConnectionURL(connectionInfo); + if (url != null && url.startsWith("jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s")) { + // Official driver. + return String.format(url, connectionInfo.getServerName(), connectionInfo.getHostName(), connectionInfo.getDatabaseName()); + } + return url; + } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { diff --git a/plugins/org.jkiss.dbeaver.ext.sqlite/src/org/jkiss/dbeaver/ext/sqlite/model/SQLiteDataSource.java b/plugins/org.jkiss.dbeaver.ext.sqlite/src/org/jkiss/dbeaver/ext/sqlite/model/SQLiteDataSource.java index 1fae66e41055c..1ccbe181244f9 100644 --- a/plugins/org.jkiss.dbeaver.ext.sqlite/src/org/jkiss/dbeaver/ext/sqlite/model/SQLiteDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.sqlite/src/org/jkiss/dbeaver/ext/sqlite/model/SQLiteDataSource.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,8 +60,9 @@ public SQLiteDataSource( } + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { return new SQLiteDataSourceInfo(container.getDriver(), metaData); } @@ -107,11 +108,12 @@ protected boolean isConnectionReadOnlyBroken() { return true; } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { diff --git a/plugins/org.jkiss.dbeaver.ext.starrocks/src/org/jkiss/dbeaver/ext/starrocks/model/StarRocksDataSource.java b/plugins/org.jkiss.dbeaver.ext.starrocks/src/org/jkiss/dbeaver/ext/starrocks/model/StarRocksDataSource.java index b8536c9fc4f4b..c4b266bf4328f 100644 --- a/plugins/org.jkiss.dbeaver.ext.starrocks/src/org/jkiss/dbeaver/ext/starrocks/model/StarRocksDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.starrocks/src/org/jkiss/dbeaver/ext/starrocks/model/StarRocksDataSource.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,8 +73,9 @@ public StarRocksMetaModel getMetaModel() { return (StarRocksMetaModel) super.getMetaModel(); } + @NotNull @Override - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) { + protected JDBCExecutionContext createExecutionContext(@NotNull JDBCRemoteInstance instance, @NotNull String type) { return new StarRocksExecutionContext(instance, type); } diff --git a/plugins/org.jkiss.dbeaver.ext.tidb/src/org/jkiss/dbeaver/ext/tidb/mysql/model/TiDBMySQLDataSource.java b/plugins/org.jkiss.dbeaver.ext.tidb/src/org/jkiss/dbeaver/ext/tidb/mysql/model/TiDBMySQLDataSource.java index b61d9e68cf519..c93ab4d138e35 100644 --- a/plugins/org.jkiss.dbeaver.ext.tidb/src/org/jkiss/dbeaver/ext/tidb/mysql/model/TiDBMySQLDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.tidb/src/org/jkiss/dbeaver/ext/tidb/mysql/model/TiDBMySQLDataSource.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -132,8 +132,9 @@ public boolean supportsInformationSchema() { return true; } + @NotNull @Override - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { + protected DBPDataSourceInfo createDataSourceInfo(@NotNull DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) { super.createDataSourceInfo(monitor, metaData); return new TiDBMySQLDataSourceInfo(this, metaData); } @@ -154,11 +155,12 @@ public boolean isServerVersionAtLeast(int major, int minor) { return true; } + @NotNull @Override protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { diff --git a/plugins/org.jkiss.dbeaver.ext.tidblake/src/org/jkiss/dbeaver/ext/tidblake/model/TiDBLakeDataSource.java b/plugins/org.jkiss.dbeaver.ext.tidblake/src/org/jkiss/dbeaver/ext/tidblake/model/TiDBLakeDataSource.java index aced14cd97764..47cce44298564 100644 --- a/plugins/org.jkiss.dbeaver.ext.tidblake/src/org/jkiss/dbeaver/ext/tidblake/model/TiDBLakeDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.tidblake/src/org/jkiss/dbeaver/ext/tidblake/model/TiDBLakeDataSource.java @@ -17,6 +17,7 @@ package org.jkiss.dbeaver.ext.tidblake.model; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.ext.generic.model.GenericDataSource; import org.jkiss.dbeaver.model.DBPDataSourceContainer; @@ -46,9 +47,12 @@ protected boolean isPopulateClientAppName() { @NotNull @Override - protected Properties getAllConnectionProperties(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, @NotNull - String purpose, @NotNull DBPConnectionConfiguration connectionInfo) - throws DBCException { + protected Properties getAllConnectionProperties( + @NotNull DBRProgressMonitor monitor, + @Nullable JDBCExecutionContext context, + @NotNull String purpose, + @NotNull DBPConnectionConfiguration connectionInfo + ) throws DBCException { Properties properties = super.getAllConnectionProperties(monitor, context, purpose, connectionInfo); final DBWHandlerConfiguration sslConfig = getContainer().getActualConnectionConfiguration().getHandler("tidblake-ssl"); diff --git a/plugins/org.jkiss.dbeaver.ext.vertica/src/org/jkiss/dbeaver/ext/vertica/model/VerticaDataSource.java b/plugins/org.jkiss.dbeaver.ext.vertica/src/org/jkiss/dbeaver/ext/vertica/model/VerticaDataSource.java index 321d1888fd440..5345beca0d580 100644 --- a/plugins/org.jkiss.dbeaver.ext.vertica/src/org/jkiss/dbeaver/ext/vertica/model/VerticaDataSource.java +++ b/plugins/org.jkiss.dbeaver.ext.vertica/src/org/jkiss/dbeaver/ext/vertica/model/VerticaDataSource.java @@ -84,7 +84,7 @@ protected boolean isPopulateClientAppName() { @Override protected Properties getAllConnectionProperties( @NotNull DBRProgressMonitor monitor, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { diff --git a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/JDBCDataSource.java b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/JDBCDataSource.java index 6884a80ee2fa6..c9b702cf9492f 100644 --- a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/JDBCDataSource.java +++ b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/JDBCDataSource.java @@ -94,15 +94,20 @@ public abstract class JDBCDataSource extends AbstractDataSource protected List tempFiles; - protected JDBCDataSource(@NotNull DBRProgressMonitor monitor, @NotNull DBPDataSourceContainer container, @NotNull SQLDialect dialect) - throws DBException - { + protected JDBCDataSource( + @NotNull DBRProgressMonitor monitor, + @NotNull DBPDataSourceContainer container, + @NotNull SQLDialect dialect + ) throws DBException { this(monitor, container, dialect, true); } - protected JDBCDataSource(@NotNull DBRProgressMonitor monitor, @NotNull DBPDataSourceContainer container, @NotNull SQLDialect dialect, boolean initContext) - throws DBException - { + protected JDBCDataSource( + @NotNull DBRProgressMonitor monitor, + @NotNull DBPDataSourceContainer container, + @NotNull SQLDialect dialect, + boolean initContext + ) throws DBException { this(container, dialect); if (initContext) { try { @@ -152,6 +157,7 @@ public void validateUserPassword( connectionInfo.setUserPassword(userPassword); try (Connection ignored = openConnectionForPasswordValidation(monitor, connectionInfo)) { // The successful connection validates the supplied credentials. + log.trace("Connection has been established"); } catch (SQLException e) { throw new DBCException("Error validating current user password", e); } @@ -313,7 +319,7 @@ protected Connection openConnection( } } - private void initializeDriverContext(Driver driverInstance) { + private void initializeDriverContext(@NotNull Driver driverInstance) { if (driverInstance instanceof ObjectWithContextParameters owcp) { DBPProject project = getContainer().getProject(); owcp.setObjectContextParameter(DBConstants.CONTEXT_PARAMETER_PROJECT, getContainer().getProject()); @@ -416,7 +422,7 @@ protected void fillConnectionProperties( @NotNull protected Properties getAllConnectionProperties( @NotNull DBRProgressMonitor monitor, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { @@ -496,7 +502,11 @@ public boolean closeConnection(final Connection connection, String purpose, bool } } - protected void initializeContextState(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, JDBCExecutionContext initFrom) throws DBException { + protected void initializeContextState( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext initFrom + ) throws DBException { } @@ -531,16 +541,24 @@ public JDBCFactory getJdbcFactory() { @NotNull @Override public JDBCRemoteInstance getDefaultInstance() { - return defaultRemoteInstance; + JDBCRemoteInstance instance = defaultRemoteInstance; + if (instance == null) { + throw new IllegalStateException("Default JDBC instance is not initialized"); + } + return instance; + } + + @Override + public boolean hasDefaultInstance() { + return defaultRemoteInstance != null; } @NotNull @Override public List getAvailableInstances() { - JDBCRemoteInstance defaultInstance = getDefaultInstance(); - return defaultInstance == null ? + return !hasDefaultInstance() ? Collections.emptyList() : - Collections.singletonList(defaultInstance); + Collections.singletonList(getDefaultInstance()); } @Override @@ -575,10 +593,10 @@ public void shutdown(@NotNull DBRProgressMonitor monitor) { @Override public void initialize(@NotNull DBRProgressMonitor monitor) throws DBException { - JDBCRemoteInstance defaultInstance = getDefaultInstance(); - if (defaultInstance == null) { + if (!hasDefaultInstance()) { throw new DBCException("Can't obtain default instance"); } + JDBCRemoteInstance defaultInstance = getDefaultInstance(); defaultInstance.initializeMetaContext(monitor); try (JDBCSession session = DBUtils.openMetaSession(monitor, this, ModelMessages.model_jdbc_read_database_meta_data)) { JDBCDatabaseMetaData metaData = session.getMetaData(); @@ -682,24 +700,21 @@ public DBSObject refreshObject(@NotNull DBRProgressMonitor monitor) throws DBExc return this; } - protected JDBCExecutionContext createExecutionContext(JDBCRemoteInstance instance, String type) throws DBCException { + @NotNull + protected JDBCExecutionContext createExecutionContext( + @NotNull JDBCRemoteInstance instance, + @NotNull String type + ) throws DBCException { return new JDBCExecutionContext(instance, type); } @Nullable @Override - public DBCQueryTransformer createQueryTransformer(@NotNull DBCQueryTransformType type) - { -// if (type == DBCQueryTransformType.ORDER_BY) { -// -// } else if (type == DBCQueryTransformType.FILTER) { -// -// } + public DBCQueryTransformer createQueryTransformer(@NotNull DBCQueryTransformType type) { return null; } - private static int getValueTypeByTypeName(@NotNull String typeName, int valueType) - { + private static int getValueTypeByTypeName(@NotNull String typeName, int valueType) { // [JDBC: SQLite driver uses VARCHAR value type for all LOBs] if (valueType == Types.OTHER || valueType == Types.VARCHAR) { if ("BLOB".equalsIgnoreCase(typeName)) { @@ -732,8 +747,10 @@ public static DBPDataKind getDataKind(@NotNull String typeName, int valueType) { @Nullable @Override - public DBSDataType resolveDataType(@NotNull DBRProgressMonitor monitor, @NotNull String typeFullName) throws DBException - { + public DBSDataType resolveDataType( + @NotNull DBRProgressMonitor monitor, + @NotNull String typeFullName + ) throws DBException { return getLocalDataType(typeFullName); } @@ -750,8 +767,7 @@ public DBSDataType getLocalDataType(int typeID) { @NotNull @Override - public String getDefaultDataTypeName(@NotNull DBPDataKind dataKind) - { + public String getDefaultDataTypeName(@NotNull DBPDataKind dataKind) { String typeName = getStandardSQLDataTypeName(dataKind); DBSDataType dataType = getLocalDataType(typeName); if (dataType == null) { @@ -778,10 +794,8 @@ protected boolean isConnectionReadOnlyBroken() { return false; } - @Nullable - protected Driver getDriverInstance(@NotNull DBRProgressMonitor monitor) - throws DBException - { + @NotNull + protected Driver getDriverInstance(@NotNull DBRProgressMonitor monitor) throws DBException { final DBPDriverSubstitutionDescriptor driverSubstitution = container.getDriverSubstitution(); if (driverSubstitution != null) { return driverSubstitution.getInstance().getSubstitutingDriverInstance(monitor); @@ -799,15 +813,18 @@ protected Driver getDriverInstance(@NotNull DBRProgressMonitor monitor) protected Map getInternalConnectionProperties( @NotNull DBRProgressMonitor monitor, @NotNull DBPDriver driver, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { return null; } - protected DBPDataSourceInfo createDataSourceInfo(DBRProgressMonitor monitor, @NotNull JDBCDatabaseMetaData metaData) - { + @NotNull + protected DBPDataSourceInfo createDataSourceInfo( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCDatabaseMetaData metaData + ) { return new JDBCDataSourceInfo(metaData); } @@ -821,8 +838,7 @@ protected JDBCFactory createJdbcFactory() { @NotNull @Override - public ErrorType discoverErrorType(@NotNull Throwable error) - { + public ErrorType discoverErrorType(@NotNull Throwable error) { String sqlState = SQLState.getStateFromException(error); if (sqlState != null) { if (SQLState.SQL_HY008.getCode().equals(sqlState)) { @@ -869,9 +885,13 @@ public ErrorPosition[] getErrorPosition( return null; } + @Nullable @Override public T getAdapter(@NotNull Class adapter) { if (adapter == DBCTransactionManager.class) { + if (!hasDefaultInstance()) { + return null; + } return adapter.cast(DBUtils.getDefaultContext(getDefaultInstance(), false)); } else if (adapter == DBCQueryTransformProvider.class) { return adapter.cast(this); @@ -882,7 +902,10 @@ public T getAdapter(@NotNull Class adapter) { ///////////////////////////////////////////////// // Canceling - public void cancelStatementExecute(DBRProgressMonitor monitor, JDBCStatement statement) throws DBException { + public void cancelStatementExecute( + @NotNull DBRProgressMonitor monitor, + @NotNull JDBCStatement statement + ) throws DBException { try { statement.cancel(); } catch (SQLException e) { @@ -894,7 +917,10 @@ public void cancelStatementExecute(DBRProgressMonitor monitor, JDBCStatement sta } } - public boolean cancelCurrentExecution(@NotNull Connection connection, @Nullable Thread connectionThread) throws DBException { + public boolean cancelCurrentExecution( + @NotNull Connection connection, + @Nullable Thread connectionThread + ) throws DBException { if (connectionThread != null) { connectionThread.interrupt(); } @@ -904,7 +930,8 @@ public boolean cancelCurrentExecution(@NotNull Connection connection, @Nullable ///////////////////////////////////////////////// // Certs - protected String saveCertificateToFile(String rootCertProp) throws IOException { + @NotNull + protected String saveCertificateToFile(@NotNull String rootCertProp) throws IOException { Path certPath = Files.createTempFile( DBWorkbench.getPlatform().getCertificateStorage().getStorageFolder(), getContainer().getDriver().getId() + "-" + getContainer().getId(), @@ -914,7 +941,8 @@ protected String saveCertificateToFile(String rootCertProp) throws IOException { return certPath.toAbsolutePath().toString(); } - protected String saveTrustStoreToFile(byte[] trustStoreData) throws IOException { + @NotNull + protected String saveTrustStoreToFile(@NotNull byte[] trustStoreData) throws IOException { Path trustStorePath = Files.createTempFile( DBWorkbench.getPlatform().getCertificateStorage().getStorageFolder(), getContainer().getDriver().getId() + "-" + getContainer().getId(), @@ -924,7 +952,7 @@ protected String saveTrustStoreToFile(byte[] trustStoreData) throws IOException return trustStorePath.toAbsolutePath().toString(); } - public void trackTempFile(Path file) { + public void trackTempFile(@NotNull Path file) { if (this.tempFiles == null) { this.tempFiles = new ArrayList<>(); } diff --git a/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/completion/SQLCompletionAnalyzer.java b/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/completion/SQLCompletionAnalyzer.java index 86c774ef2b17a..c48c6b94c5a0b 100644 --- a/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/completion/SQLCompletionAnalyzer.java +++ b/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/completion/SQLCompletionAnalyzer.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -642,9 +642,11 @@ private void filterProposals(DBPDataSource dataSource) { i++; } - DBSInstance defaultInstance = dataSource == null ? null : dataSource.getDefaultInstance(); + DBSInstance defaultInstance = dataSource == null || !dataSource.hasDefaultInstance() + ? null : dataSource.getDefaultInstance(); DBCExecutionContext executionContext = request.getContext().getExecutionContext(); - DBSObject selectedObject = defaultInstance == null || executionContext == null ? null : DBUtils.getActiveInstanceObject(executionContext); + DBSObject selectedObject = defaultInstance == null || executionContext == null + ? null : DBUtils.getActiveInstanceObject(executionContext); boolean hideDups = request.getContext().isHideDuplicates() && selectedObject != null; if (hideDups) { for (int i = 0; i < proposals.size(); i++) { diff --git a/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/semantics/context/SQLQueryConnectionDummyContext.java b/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/semantics/context/SQLQueryConnectionDummyContext.java index cf1efdb77d5a0..7f02d2696a628 100644 --- a/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/semantics/context/SQLQueryConnectionDummyContext.java +++ b/plugins/org.jkiss.dbeaver.model.sql/src/org/jkiss/dbeaver/model/sql/semantics/context/SQLQueryConnectionDummyContext.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -246,6 +246,11 @@ public DBSInstance getDefaultInstance() { return null; } + @Override + public boolean hasDefaultInstance() { + return false; + } + @NotNull @Override public Collection getAvailableInstances() { diff --git a/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/model/DBUtils.java b/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/model/DBUtils.java index 0e3388d736fd7..f47c81936ea54 100644 --- a/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/model/DBUtils.java +++ b/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/model/DBUtils.java @@ -2378,7 +2378,7 @@ public static DBSInstance getObjectOwnerInstance(DBSObject object) { } } DBPDataSource dataSource = object.getDataSource(); - if (dataSource == null || dataSource.isConnectionRefreshing()) { + if (dataSource == null || dataSource.isConnectionRefreshing() || !dataSource.hasDefaultInstance()) { return null; } return dataSource.getDefaultInstance(); diff --git a/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/model/struct/DBSInstanceContainer.java b/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/model/struct/DBSInstanceContainer.java index a92685892f9c0..6b1d3db98aca2 100644 --- a/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/model/struct/DBSInstanceContainer.java +++ b/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/model/struct/DBSInstanceContainer.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,11 @@ public interface DBSInstanceContainer extends DBSObject { @NotNull Collection getAvailableInstances(); + /** Override when the default instance can be unavailable. */ + default boolean hasDefaultInstance() { + return true; + } + void shutdown(@NotNull DBRProgressMonitor monitor); } diff --git a/plugins/org.jkiss.dbeaver.registry/src/org/jkiss/dbeaver/registry/DataSourceDescriptor.java b/plugins/org.jkiss.dbeaver.registry/src/org/jkiss/dbeaver/registry/DataSourceDescriptor.java index c8930ec7d9522..c5a5c2f1a3f86 100644 --- a/plugins/org.jkiss.dbeaver.registry/src/org/jkiss/dbeaver/registry/DataSourceDescriptor.java +++ b/plugins/org.jkiss.dbeaver.registry/src/org/jkiss/dbeaver/registry/DataSourceDescriptor.java @@ -546,17 +546,15 @@ public void resetAllSecrets() { @Nullable @Override public DBPTransactionIsolation getActiveTransactionsIsolation() { - if (dataSource != null) { + if (dataSource != null && dataSource.hasDefaultInstance()) { DBSInstance defaultInstance = dataSource.getDefaultInstance(); - if (defaultInstance != null) { - DBCTransactionManager txnManager = DBUtils.getTransactionManager(DBUtils.getDefaultContext(defaultInstance, false)); - if (txnManager != null) { - try { - return txnManager.getTransactionIsolation(); - } catch (DBCException e) { - log.debug("Can't determine isolation level", e); - return null; - } + DBCTransactionManager txnManager = DBUtils.getTransactionManager(DBUtils.getDefaultContext(defaultInstance, false)); + if (txnManager != null) { + try { + return txnManager.getTransactionIsolation(); + } catch (DBCException e) { + log.debug("Can't determine isolation level", e); + return null; } } } diff --git a/plugins/org.jkiss.dbeaver.registry/src/org/jkiss/dbeaver/registry/DataSourceProviderRegistry.java b/plugins/org.jkiss.dbeaver.registry/src/org/jkiss/dbeaver/registry/DataSourceProviderRegistry.java index 683580eee636d..93b140b499b83 100644 --- a/plugins/org.jkiss.dbeaver.registry/src/org/jkiss/dbeaver/registry/DataSourceProviderRegistry.java +++ b/plugins/org.jkiss.dbeaver.registry/src/org/jkiss/dbeaver/registry/DataSourceProviderRegistry.java @@ -459,8 +459,8 @@ public DataSourceTypeDescriptor resolveDataSourceType(@Nullable String typeId, @ } DataSourceTypeDescriptor type = dataSourceTypes.get(resolvedId); if (type == null) { - // Shouldn't be here - log.debug("Create new datasource type '" + resolvedId + "'"); + // Shouldn't be here. But it happens in unit tests. + log.trace("Create new datasource type '" + resolvedId + "'"); type = new DataSourceTypeDescriptor(resolvedId, driver); dataSourceTypes.put(resolvedId, type); } diff --git a/plugins/org.jkiss.dbeaver.ui.dashboard/src/org/jkiss/dbeaver/ui/dashboard/control/DashboardListViewer.java b/plugins/org.jkiss.dbeaver.ui.dashboard/src/org/jkiss/dbeaver/ui/dashboard/control/DashboardListViewer.java index abcfabd777d34..5969c745266eb 100644 --- a/plugins/org.jkiss.dbeaver.ui.dashboard/src/org/jkiss/dbeaver/ui/dashboard/control/DashboardListViewer.java +++ b/plugins/org.jkiss.dbeaver.ui.dashboard/src/org/jkiss/dbeaver/ui/dashboard/control/DashboardListViewer.java @@ -194,7 +194,9 @@ public DBCExecutionContext getExecutionContext() { if (dataSourceContainer == null) { return null; } - return DBUtils.getDefaultContext(dataSourceContainer.getDataSource().getDefaultInstance(), true); + DBPDataSource dataSource = dataSourceContainer.getDataSource(); + return dataSource == null || !dataSource.hasDefaultInstance() + ? null : DBUtils.getDefaultContext(dataSource.getDefaultInstance(), true); } @NotNull diff --git a/plugins/org.jkiss.dbeaver.ui.editors.sql/src/org/jkiss/dbeaver/ui/editors/sql/preferences/PrefPageSQLFormat.java b/plugins/org.jkiss.dbeaver.ui.editors.sql/src/org/jkiss/dbeaver/ui/editors/sql/preferences/PrefPageSQLFormat.java index 113dc5f5516ff..2334c4c110299 100644 --- a/plugins/org.jkiss.dbeaver.ui.editors.sql/src/org/jkiss/dbeaver/ui/editors/sql/preferences/PrefPageSQLFormat.java +++ b/plugins/org.jkiss.dbeaver.ui.editors.sql/src/org/jkiss/dbeaver/ui/editors/sql/preferences/PrefPageSQLFormat.java @@ -158,7 +158,7 @@ public DBCExecutionContext getExecutionContext() { final DBPDataSourceContainer container = getDataSourceContainer(); if (container != null) { final DBPDataSource dataSource = container.getDataSource(); - if (dataSource != null) { + if (dataSource != null && dataSource.hasDefaultInstance()) { return DBUtils.getDefaultContext(dataSource.getDefaultInstance(), false); } } diff --git a/plugins/org.jkiss.dbeaver.ui.navigator/src/org/jkiss/dbeaver/ui/navigator/actions/NavigatorHandlerLinkEditor.java b/plugins/org.jkiss.dbeaver.ui.navigator/src/org/jkiss/dbeaver/ui/navigator/actions/NavigatorHandlerLinkEditor.java index b459712195573..066b0664091b3 100644 --- a/plugins/org.jkiss.dbeaver.ui.navigator/src/org/jkiss/dbeaver/ui/navigator/actions/NavigatorHandlerLinkEditor.java +++ b/plugins/org.jkiss.dbeaver.ui.navigator/src/org/jkiss/dbeaver/ui/navigator/actions/NavigatorHandlerLinkEditor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -90,7 +90,7 @@ public Object execute(ExecutionEvent event) throws ExecutionException { } if (activeObject == null) { DBPDataSource dataSource = dsContainer.getDataSource(); - if (dataSource != null) { + if (dataSource != null && dataSource.hasDefaultInstance()) { activeObject = DBUtils.getDefaultOrActiveObject(dataSource.getDefaultInstance()); } else { activeObject = dsContainer; @@ -134,4 +134,4 @@ private void showResourceInNavigator(NavigatorViewBase activePart, IFile editorF activePart.showNode(resource); } } -} \ No newline at end of file +} diff --git a/test/org.jkiss.dbeaver.ext.generic.test/src/org/jkiss/dbeaver/ext/generic/test/GenericBaseTableDDLTest.java b/test/org.jkiss.dbeaver.ext.generic.test/src/org/jkiss/dbeaver/ext/generic/test/GenericBaseTableDDLTest.java index f8b3e0f9ed7fd..c10a6d1f0778b 100644 --- a/test/org.jkiss.dbeaver.ext.generic.test/src/org/jkiss/dbeaver/ext/generic/test/GenericBaseTableDDLTest.java +++ b/test/org.jkiss.dbeaver.ext.generic.test/src/org/jkiss/dbeaver/ext/generic/test/GenericBaseTableDDLTest.java @@ -22,6 +22,7 @@ import org.jkiss.dbeaver.model.DBPDataSourceContainer; import org.jkiss.dbeaver.model.edit.DBEObjectMaker; import org.jkiss.dbeaver.model.edit.DBEPersistAction; +import org.jkiss.dbeaver.model.exec.DBCTransactionManager; import org.jkiss.dbeaver.model.exec.DBExecUtils; import org.jkiss.dbeaver.model.impl.edit.TestCommandContext; import org.jkiss.dbeaver.model.impl.jdbc.JDBCRemoteInstance; @@ -72,6 +73,14 @@ private DBEObjectMaker getManagerForClass(Class objectClass) { return DBWorkbench.getPlatform().getEditorsRegistry().getObjectManager(objectClass, DBEObjectMaker.class); } + @Test + public void uninitializedDataSourceHasNoDefaultInstance() { + Assertions.assertFalse(dataSource.hasDefaultInstance()); + Assertions.assertThrows(IllegalStateException.class, dataSource::getDefaultInstance); + Assertions.assertTrue(dataSource.getAvailableInstances().isEmpty()); + Assertions.assertNull(dataSource.getAdapter(DBCTransactionManager.class)); + } + @Test public void generateCreateNewTableWithTwoColumnsStatement() throws Exception { TestCommandContext commandContext = new TestCommandContext(executionContext, false); diff --git a/test/org.jkiss.dbeaver.ext.generic.test/src/org/jkiss/dbeaver/ext/generic/test/VerticaSSLUtilsTest.java b/test/org.jkiss.dbeaver.ext.generic.test/src/org/jkiss/dbeaver/ext/generic/test/VerticaSSLUtilsTest.java index a4464a6606335..1c935f0c5b191 100644 --- a/test/org.jkiss.dbeaver.ext.generic.test/src/org/jkiss/dbeaver/ext/generic/test/VerticaSSLUtilsTest.java +++ b/test/org.jkiss.dbeaver.ext.generic.test/src/org/jkiss/dbeaver/ext/generic/test/VerticaSSLUtilsTest.java @@ -17,6 +17,7 @@ package org.jkiss.dbeaver.ext.generic.test; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.ext.generic.model.meta.GenericMetaModel; import org.jkiss.dbeaver.ext.vertica.VerticaConstants; @@ -156,7 +157,7 @@ public TestVerticaDataSource( @Override public Properties getAllConnectionProperties( @NotNull DBRProgressMonitor monitor, - @NotNull JDBCExecutionContext context, + @Nullable JDBCExecutionContext context, @NotNull String purpose, @NotNull DBPConnectionConfiguration connectionInfo ) throws DBCException { diff --git a/test/org.jkiss.dbeaver.ext.greenplum.test/src/org/jkiss/dbeaver/ext/greenplum/model/GreenplumTableTest.java b/test/org.jkiss.dbeaver.ext.greenplum.test/src/org/jkiss/dbeaver/ext/greenplum/model/GreenplumTableTest.java index 77fd8956aa672..eb70e83e6b600 100644 --- a/test/org.jkiss.dbeaver.ext.greenplum.test/src/org/jkiss/dbeaver/ext/greenplum/model/GreenplumTableTest.java +++ b/test/org.jkiss.dbeaver.ext.greenplum.test/src/org/jkiss/dbeaver/ext/greenplum/model/GreenplumTableTest.java @@ -81,6 +81,7 @@ public void setUp() throws Exception { Mockito.when(mockDataSource.getSQLDialect()).thenReturn(new PostgreDialect()); Mockito.when(mockDataSource.isServerVersionAtLeast(Mockito.anyInt(), Mockito.anyInt())).thenReturn(false); + Mockito.when(mockDataSource.hasDefaultInstance()).thenReturn(true); Mockito.when(mockDataSource.getDefaultInstance()).thenReturn(mockDatabase); Mockito.when(mockDataSource.getServerType()).thenReturn(mockServerGreenplum); @@ -213,4 +214,4 @@ private List createMockColumns(String... columns) { return mockColumn; }).collect(Collectors.toList()); } -} \ No newline at end of file +} diff --git a/test/org.jkiss.dbeaver.ext.mimer.test/src/org/jkiss/dbeaver/ext/mimer/model/MimerStatementTest.java b/test/org.jkiss.dbeaver.ext.mimer.test/src/org/jkiss/dbeaver/ext/mimer/model/MimerStatementTest.java index da710118140d6..37903400253bc 100644 --- a/test/org.jkiss.dbeaver.ext.mimer.test/src/org/jkiss/dbeaver/ext/mimer/model/MimerStatementTest.java +++ b/test/org.jkiss.dbeaver.ext.mimer.test/src/org/jkiss/dbeaver/ext/mimer/model/MimerStatementTest.java @@ -73,6 +73,7 @@ public class MimerStatementTest extends DBeaverUnitTest { public void setUp() throws Exception { Mockito.when(schema.getName()).thenReturn("mimer_store"); Mockito.when(schema.getDataSource()).thenReturn(dataSource); + Mockito.when(dataSource.hasDefaultInstance()).thenReturn(true); Mockito.when(dataSource.getDefaultInstance()).thenReturn(instance); Mockito.when(instance.getDefaultContext(ArgumentMatchers.any(), ArgumentMatchers.anyBoolean())).thenReturn(executionContext); Mockito.when(executionContext.openSession( diff --git a/test/org.jkiss.dbeaver.ext.mimer.test/src/org/jkiss/dbeaver/ext/mimer/model/MimerUtilsExternalRoutineTest.java b/test/org.jkiss.dbeaver.ext.mimer.test/src/org/jkiss/dbeaver/ext/mimer/model/MimerUtilsExternalRoutineTest.java index c860d48176a7d..87cea25c19006 100644 --- a/test/org.jkiss.dbeaver.ext.mimer.test/src/org/jkiss/dbeaver/ext/mimer/model/MimerUtilsExternalRoutineTest.java +++ b/test/org.jkiss.dbeaver.ext.mimer.test/src/org/jkiss/dbeaver/ext/mimer/model/MimerUtilsExternalRoutineTest.java @@ -214,6 +214,7 @@ public void escapesASingleQuoteInTheExternalName() throws Exception { */ private void mockDatabaseChain() throws Exception { Mockito.when(dataSource.supportsExternalLibraries()).thenReturn(true); + Mockito.when(dataSource.hasDefaultInstance()).thenReturn(true); Mockito.when(dataSource.getDefaultInstance()).thenReturn(instance); Mockito.when(instance.getDefaultContext(ArgumentMatchers.any(), ArgumentMatchers.anyBoolean())).thenReturn(executionContext); Mockito.when(executionContext.openSession( diff --git a/test/org.jkiss.dbeaver.ext.oracle.test/src/org/jkiss/dbeaver/ext/oracle/model/OracleUserProfileTest.java b/test/org.jkiss.dbeaver.ext.oracle.test/src/org/jkiss/dbeaver/ext/oracle/model/OracleUserProfileTest.java index 6f17ff45ca18a..a401c7282fc11 100644 --- a/test/org.jkiss.dbeaver.ext.oracle.test/src/org/jkiss/dbeaver/ext/oracle/model/OracleUserProfileTest.java +++ b/test/org.jkiss.dbeaver.ext.oracle.test/src/org/jkiss/dbeaver/ext/oracle/model/OracleUserProfileTest.java @@ -52,6 +52,7 @@ public void setUp() throws SQLException { statement = Mockito.mock(JDBCPreparedStatement.class); resultSet = Mockito.mock(JDBCResultSet.class); JDBCRemoteInstance instance = Mockito.mock(JDBCRemoteInstance.class); + Mockito.when(dataSource.hasDefaultInstance()).thenReturn(true); Mockito.when(dataSource.getDefaultInstance()).thenReturn(instance); JDBCExecutionContext context = Mockito.mock(JDBCExecutionContext.class); Mockito.when(instance.getDefaultContext(monitor, true)).thenReturn(context); From 72ee407c9aeae47a92eb9a91235dc2b99c22f63a Mon Sep 17 00:00:00 2001 From: Isabella Tessarotto Date: Sun, 18 Oct 2026 11:27:28 -0300 Subject: [PATCH 2/7] dbeaver/dbeaver#42474 Add metadata snapshot store for JDBC caches Assisted by: Claude Code --- .../impl/jdbc/cache/JDBCMetadataSnapshot.java | 923 ++++++++++++++++++ .../META-INF/MANIFEST.MF | 13 + .../build.properties | 5 + .../org.jkiss.dbeaver.model.jdbc.test/pom.xml | 32 + .../jdbc/cache/JDBCMetadataSnapshotTest.java | 287 ++++++ test/pom.xml | 1 + 6 files changed, 1261 insertions(+) create mode 100644 plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshot.java create mode 100644 test/org.jkiss.dbeaver.model.jdbc.test/META-INF/MANIFEST.MF create mode 100644 test/org.jkiss.dbeaver.model.jdbc.test/build.properties create mode 100644 test/org.jkiss.dbeaver.model.jdbc.test/pom.xml create mode 100644 test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java diff --git a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshot.java b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshot.java new file mode 100644 index 0000000000000..cb44712df9c7e --- /dev/null +++ b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshot.java @@ -0,0 +1,923 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed 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.jkiss.dbeaver.model.impl.jdbc.cache; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; +import org.jkiss.dbeaver.model.exec.jdbc.JDBCResultSet; +import org.jkiss.dbeaver.model.exec.jdbc.JDBCSession; +import org.jkiss.dbeaver.model.impl.jdbc.exec.JDBCResultSetImpl; +import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; +import org.jkiss.dbeaver.model.runtime.AbstractJob; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.utils.CommonUtils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * Local copy of metadata rows of one cache owner (e.g. a schema), kept on disk between sessions. + * + *

The snapshot stores the rows returned by the metadata queries, not the objects. Restored rows are replayed + * as a result set through the same {@code fetchObject} code as rows read from the database, so a restored object + * is built exactly like a loaded one.

+ * + *

Rows are kept in sets (one per kind of metadata, e.g. tables or columns) and grouped by object name: + * a table list has one row per group, a column list has all columns of one table in a group. + * Every group keeps the (client) time its rows were read at, for drivers which can tell what changed since then.

+ */ +public class JDBCMetadataSnapshot { + private static final Log log = Log.getLog(JDBCMetadataSnapshot.class); + + private static final int FORMAT_VERSION = 1; + private static final String SNAPSHOT_FOLDER = "metadata-snapshots"; + private static final long SAVE_DELAY_MS = 3000; + + /** + * Reads rows of a replayed result set into objects. + */ + @FunctionalInterface + public interface RowFetcher { + @Nullable + T fetch(@NotNull JDBCResultSet resultSet) throws SQLException, DBException; + } + + /** + * Reads rows of a replayed result set into objects, for rows of a group. + */ + @FunctionalInterface + public interface GroupFetcher { + @Nullable + T fetch(@NotNull String group, @NotNull JDBCResultSet resultSet) throws SQLException, DBException; + } + + /** + * Column labels of a stored row set. Date and time columns are stored as epoch milliseconds, all others as strings. + */ + static final class Layout { + final String[] labels; + final boolean[] timestamps; + private final Map index = new HashMap<>(); + + Layout(@NotNull String[] labels, @NotNull boolean[] timestamps) { + this.labels = labels; + this.timestamps = timestamps; + for (int i = 0; i < labels.length; i++) { + index.putIfAbsent(labels[i].toUpperCase(Locale.ENGLISH), i); + } + } + + @NotNull + static Layout of(@NotNull ResultSetMetaData meta) throws SQLException { + int count = meta.getColumnCount(); + String[] labels = new String[count]; + boolean[] timestamps = new boolean[count]; + for (int i = 0; i < count; i++) { + labels[i] = meta.getColumnLabel(i + 1); + int type = meta.getColumnType(i + 1); + timestamps[i] = type == Types.DATE || type == Types.TIMESTAMP || type == Types.TIMESTAMP_WITH_TIMEZONE; + } + return new Layout(labels, timestamps); + } + + int indexOf(@NotNull String label) { + Integer i = index.get(label.toUpperCase(Locale.ENGLISH)); + return i == null ? -1 : i; + } + + boolean sameAs(@Nullable Layout other) { + return other != null && Arrays.equals(labels, other.labels) && Arrays.equals(timestamps, other.timestamps); + } + + @NotNull + Object[] capture(@NotNull ResultSet resultSet) throws SQLException { + Object[] row = new Object[labels.length]; + for (int i = 0; i < labels.length; i++) { + if (timestamps[i]) { + Timestamp ts = resultSet.getTimestamp(i + 1); + row[i] = ts == null ? null : ts.getTime(); + } else { + row[i] = resultSet.getString(i + 1); + } + } + return row; + } + } + + private record Group(@NotNull List rows, long readTime) { + } + + private static final class RowSet { + Layout layout; + // The full list was read at least once: the set may be restored even if it has no groups + boolean complete; + final Map groups = new LinkedHashMap<>(); + } + + /** + * Rows read by one load. Stored in the snapshot only by {@link #commit()}, i.e. when the load completed: + * a canceled or failed load leaves the snapshot as it was. + */ + public final class Pass { + private final String setId; + private final boolean fullList; + private final long startTime = System.currentTimeMillis(); + private final Map> groups = new LinkedHashMap<>(); + private Layout layout; + private ResultSet lastResultSet; + private boolean failed; + + private Pass(@NotNull String setId, boolean fullList) { + this.setId = setId; + this.fullList = fullList; + } + + /** + * Stores the current row of the result set in a group. Errors are not thrown: the load goes on, + * only the pass is not committed. + */ + public void record(@NotNull String group, @NotNull ResultSet resultSet) { + if (failed) { + return; + } + try { + if (!Objects.equals(resultSet, lastResultSet)) { + Layout newLayout = Layout.of(resultSet.getMetaData()); + if (layout != null && !layout.sameAs(newLayout)) { + throw new SQLException("Different row layout in one pass"); + } + layout = newLayout; + lastResultSet = resultSet; + } + groups.computeIfAbsent(group, g -> new ArrayList<>()).add(layout.capture(resultSet)); + } catch (Exception e) { + log.debug("Metadata row of " + setId + " not stored in snapshot: " + e.getMessage()); + failed = true; + } + } + + /** + * Stores a group without rows (e.g. a table without columns). + */ + public void recordEmpty(@NotNull String group) { + groups.computeIfAbsent(group, g -> new ArrayList<>()); + } + + /** + * Whether rows were read for the group in this pass. + */ + public boolean contains(@NotNull String group) { + return groups.containsKey(group); + } + + /** + * Groups whose rows differ from the stored ones: new and changed groups and, for a full list, removed groups. + * Rows are compared as read (strings and timestamps), so any change of a metadata row is detected. + */ + @NotNull + public Set getChangedGroups() { + Set changed = new LinkedHashSet<>(); + synchronized (JDBCMetadataSnapshot.this) { + readFile(); + RowSet set = sets.get(setId); + boolean sameLayout = set != null && (layout == null || layout.sameAs(set.layout)); + for (Map.Entry> e : groups.entrySet()) { + Group stored = sameLayout ? set.groups.get(e.getKey()) : null; + if (stored == null || !sameRows(stored.rows(), e.getValue())) { + changed.add(e.getKey()); + } + } + if (fullList && set != null) { + for (String group : set.groups.keySet()) { + if (!groups.containsKey(group)) { + changed.add(group); + } + } + } + } + return changed; + } + + public void commit() { + if (failed) { + return; + } + synchronized (JDBCMetadataSnapshot.this) { + readFile(); + RowSet set = sets.computeIfAbsent(setId, id -> new RowSet()); + if (fullList || (layout != null && set.layout != null && !layout.sameAs(set.layout))) { + // Groups missing in a full list are gone. Rows of another layout cannot be kept with new ones. + set.groups.clear(); + set.complete = fullList; + } + if (layout != null) { + set.layout = layout; + } + for (Map.Entry> e : groups.entrySet()) { + set.groups.put(e.getKey(), new Group(e.getValue(), startTime)); + } + scheduleSave(); + } + } + } + + private final Path file; + private final String signature; + private final Map sets = new HashMap<>(); + private boolean fileRead; + private boolean dirty; + private SaveJob saveJob; + + public JDBCMetadataSnapshot(@NotNull Path file, @NotNull String signature) { + this.file = file; + this.signature = signature; + } + + /** + * Snapshot file of a cache owner. Kept in the workspace metadata, never in the project folder (which may be shared). + * The escaped name is lossy (e.g. "A/B" and "A_B", or case on case-insensitive file systems), + * so the hash of the exact name keeps files of different owners apart. + */ + @NotNull + public static Path getSnapshotFile(@NotNull DBPDataSourceContainer container, @NotNull String ownerId) { + return DBWorkbench.getPlatform().getWorkspace().getMetadataFolder() + .resolve(SNAPSHOT_FOLDER) + .resolve(CommonUtils.escapeFileName(container.getProject().getName())) + .resolve(CommonUtils.escapeFileName(container.getId())) + .resolve(CommonUtils.escapeFileName(ownerId) + "-" + Integer.toHexString(ownerId.hashCode()) + ".json.gz"); + } + + /** + * Connection settings which change the content of metadata rows, plus driver specific ones. + * A snapshot written with another signature is ignored. + * Navigator settings are included: caches skip rows of hidden (system or utility) objects. + */ + @NotNull + public static String makeSignature(@NotNull DBPDataSourceContainer container, @NotNull String... settings) { + DBPConnectionConfiguration cfg = container.getConnectionConfiguration(); + DBNBrowseSettings navigatorSettings = container.getNavigatorSettings(); + List parts = new ArrayList<>(); + parts.add(String.valueOf(FORMAT_VERSION)); + parts.add(container.getDriver().getFullId()); + parts.add(CommonUtils.notEmpty(cfg.getUrl())); + parts.add(CommonUtils.notEmpty(cfg.getUserName())); + parts.add(String.valueOf(navigatorSettings.isShowSystemObjects())); + parts.add(String.valueOf(navigatorSettings.isShowUtilityObjects())); + parts.addAll(Arrays.asList(settings)); + return String.join("|", parts); + } + + @NotNull + public Path getFile() { + return file; + } + + /** + * Starts recording rows of one load. + * + * @param fullList the load reads all groups of the set: stored groups missing in it are removed + */ + @NotNull + public Pass beginPass(@NotNull String setId, boolean fullList) { + return new Pass(setId, fullList); + } + + /** + * Whether all groups of the set were stored, so the set can be restored without reading the database. + */ + public synchronized boolean isComplete(@NotNull String setId) { + readFile(); + RowSet set = sets.get(setId); + return set != null && set.complete; + } + + public synchronized boolean hasGroup(@NotNull String setId, @NotNull String group) { + readFile(); + RowSet set = sets.get(setId); + return set != null && set.groups.containsKey(group); + } + + /** + * Read times of the stored groups, by group name. + */ + @NotNull + public synchronized Map getReadTimes(@NotNull String setId) { + readFile(); + RowSet set = sets.get(setId); + Map result = new LinkedHashMap<>(); + if (set != null) { + for (Map.Entry e : set.groups.entrySet()) { + result.put(e.getKey(), e.getValue().readTime()); + } + } + return result; + } + + public synchronized void removeGroup(@NotNull String setId, @NotNull String group) { + readFile(); + RowSet set = sets.get(setId); + if (set != null && set.groups.remove(group) != null) { + scheduleSave(); + } + } + + /** + * Forgets the stored rows of a set (e.g. after an explicit refresh of the owner). + */ + public synchronized void clear(@NotNull String setId) { + readFile(); + if (sets.remove(setId) != null) { + scheduleSave(); + } + } + + /** + * Builds objects from the stored rows, in the order they were read. + * + * @param group group to restore, or null for all groups of the set + * @return restored objects, or null if nothing is stored + */ + @Nullable + public List restore( + @NotNull JDBCSession session, + @NotNull String setId, + @Nullable String group, + @NotNull RowFetcher fetcher + ) throws SQLException, DBException { + Layout layout; + List rows = new ArrayList<>(); + synchronized (this) { + readFile(); + RowSet set = sets.get(setId); + if (set == null || (group == null && !set.complete) || (group != null && !set.groups.containsKey(group))) { + return null; + } + layout = set.layout; + for (Map.Entry e : set.groups.entrySet()) { + if (group == null || group.equals(e.getKey())) { + rows.addAll(e.getValue().rows()); + } + } + } + List result = new ArrayList<>(rows.size()); + if (rows.isEmpty()) { + return result; + } + RowReplay replay = new RowReplay(layout, rows); + try (JDBCResultSet resultSet = JDBCResultSetImpl.makeResultSet(session, null, replay.newResultSet(), true)) { + while (resultSet.next()) { + T object = fetcher.fetch(resultSet); + if (object != null) { + result.add(object); + } + } + } + return result; + } + + /** + * Builds objects from the stored rows of all groups of a complete set, in the order they were read. + * + * @return restored objects by group name, or null if the set is not complete + */ + @Nullable + public Map> restoreGroups( + @NotNull JDBCSession session, + @NotNull String setId, + @NotNull GroupFetcher fetcher + ) throws SQLException, DBException { + Layout layout; + List rows = new ArrayList<>(); + List rowGroups = new ArrayList<>(); + Map> result = new LinkedHashMap<>(); + synchronized (this) { + readFile(); + RowSet set = sets.get(setId); + if (set == null || !set.complete) { + return null; + } + layout = set.layout; + for (Map.Entry e : set.groups.entrySet()) { + result.put(e.getKey(), new ArrayList<>()); + for (Object[] row : e.getValue().rows()) { + rows.add(row); + rowGroups.add(e.getKey()); + } + } + } + if (rows.isEmpty()) { + return result; + } + RowReplay replay = new RowReplay(layout, rows); + try (JDBCResultSet resultSet = JDBCResultSetImpl.makeResultSet(session, null, replay.newResultSet(), true)) { + for (int i = 0; resultSet.next(); i++) { + String group = rowGroups.get(i); + T object = fetcher.fetch(group, resultSet); + if (object != null) { + result.get(group).add(object); + } + } + } + return result; + } + + /** + * Writes pending changes immediately (e.g. when the connection is closed). + */ + public void flush() { + SaveJob job; + synchronized (this) { + job = saveJob; + } + if (job != null) { + job.cancel(); + } + save(); + } + + private static boolean sameRows(@NotNull List rows1, @NotNull List rows2) { + if (rows1.size() != rows2.size()) { + return false; + } + for (int i = 0; i < rows1.size(); i++) { + if (!Arrays.equals(rows1.get(i), rows2.get(i))) { + return false; + } + } + return true; + } + + /////////////////////////////////////////////////////////////////////// + // Persistence + + /** + * Reads the snapshot file once. Every change goes through this first: the in-memory state always contains + * what is on disk, so a save never replaces stored sets with a partial content. + */ + private synchronized void readFile() { + if (fileRead) { + return; + } + fileRead = true; + if (!Files.exists(file)) { + return; + } + try (JsonReader reader = new JsonReader(new InputStreamReader( + new GZIPInputStream(new BufferedInputStream(Files.newInputStream(file), 1 << 16)), StandardCharsets.UTF_8))) { + readSnapshot(reader); + } catch (Exception e) { + log.warn("Error reading metadata snapshot " + file + ", ignoring it", e); + sets.clear(); + } + } + + private void readSnapshot(@NotNull JsonReader reader) throws IOException { + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.nextName()) { + case "version" -> { + if (reader.nextInt() != FORMAT_VERSION) { + return; + } + } + case "signature" -> { + if (!signature.equals(reader.nextString())) { + // Written with other connection settings + return; + } + } + case "sets" -> { + reader.beginObject(); + while (reader.hasNext()) { + String setId = reader.nextName(); + sets.put(setId, readSet(reader)); + } + reader.endObject(); + } + default -> reader.skipValue(); + } + } + reader.endObject(); + } + + @NotNull + private static RowSet readSet(@NotNull JsonReader reader) throws IOException { + RowSet set = new RowSet(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.nextName()) { + case "complete" -> set.complete = reader.nextBoolean(); + case "layout" -> set.layout = readLayout(reader); + case "groups" -> { + reader.beginArray(); + while (reader.hasNext()) { + readGroup(reader, set); + } + reader.endArray(); + } + default -> reader.skipValue(); + } + } + reader.endObject(); + return set; + } + + private static void readGroup(@NotNull JsonReader reader, @NotNull RowSet set) throws IOException { + String name = null; + long readTime = 0; + List rows = new ArrayList<>(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.nextName()) { + case "name" -> name = reader.nextString(); + case "read" -> readTime = reader.nextLong(); + case "rows" -> { + reader.beginArray(); + while (reader.hasNext()) { + rows.add(readRow(reader, set.layout)); + } + reader.endArray(); + } + default -> reader.skipValue(); + } + } + reader.endObject(); + if (name == null) { + throw new IOException("Group without name"); + } + set.groups.put(name, new Group(rows, readTime)); + } + + @NotNull + private static Layout readLayout(@NotNull JsonReader reader) throws IOException { + List labels = new ArrayList<>(); + String types = ""; + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.nextName()) { + case "labels" -> { + reader.beginArray(); + while (reader.hasNext()) { + labels.add(reader.nextString()); + } + reader.endArray(); + } + case "types" -> types = reader.nextString(); + default -> reader.skipValue(); + } + } + reader.endObject(); + if (types.length() != labels.size()) { + throw new IOException("Broken row layout"); + } + boolean[] timestamps = new boolean[labels.size()]; + for (int i = 0; i < timestamps.length; i++) { + timestamps[i] = types.charAt(i) == 'T'; + } + return new Layout(labels.toArray(new String[0]), timestamps); + } + + @NotNull + private static Object[] readRow(@NotNull JsonReader reader, @Nullable Layout layout) throws IOException { + if (layout == null) { + throw new IOException("Row without layout"); + } + Object[] row = new Object[layout.labels.length]; + reader.beginArray(); + for (int i = 0; reader.hasNext(); i++) { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + } else if (i < row.length) { + row[i] = layout.timestamps[i] ? (Object) reader.nextLong() : reader.nextString(); + } else { + reader.skipValue(); + } + } + reader.endArray(); + return row; + } + + private synchronized void scheduleSave() { + dirty = true; + if (saveJob == null) { + saveJob = new SaveJob(); + } + // Rescheduling postpones the save: a burst of changes is written once + saveJob.schedule(SAVE_DELAY_MS); + } + + private void save() { + try { + writeFile(); + } catch (IOException e) { + log.warn("Error saving metadata snapshot " + file, e); + } + } + + private void writeFile() throws IOException { + Path tmpFile = file.resolveSibling(file.getFileName() + ".tmp"); + synchronized (this) { + if (!dirty) { + return; + } + Files.createDirectories(file.getParent()); + try (JsonWriter writer = new JsonWriter(new OutputStreamWriter( + new GZIPOutputStream(new BufferedOutputStream(Files.newOutputStream(tmpFile), 1 << 16)), StandardCharsets.UTF_8))) { + writeSnapshot(writer); + } + dirty = false; + } + // Readers never see a partially written snapshot + try { + Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + // Some file systems (e.g. network workspaces) cannot move atomically + Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING); + } + } + + private void writeSnapshot(@NotNull JsonWriter writer) throws IOException { + writer.beginObject(); + writer.name("version").value(FORMAT_VERSION); + writer.name("signature").value(signature); + writer.name("sets").beginObject(); + for (Map.Entry e : sets.entrySet()) { + RowSet set = e.getValue(); + writer.name(e.getKey()).beginObject(); + writer.name("complete").value(set.complete); + if (set.layout != null) { + writer.name("layout"); + writeLayout(writer, set.layout); + } + writer.name("groups").beginArray(); + for (Map.Entry g : set.groups.entrySet()) { + writer.beginObject(); + writer.name("name").value(g.getKey()); + writer.name("read").value(g.getValue().readTime()); + writer.name("rows").beginArray(); + for (Object[] row : g.getValue().rows()) { + writeRow(writer, row); + } + writer.endArray(); + writer.endObject(); + } + writer.endArray(); + writer.endObject(); + } + writer.endObject(); + writer.endObject(); + } + + private static void writeLayout(@NotNull JsonWriter writer, @NotNull Layout layout) throws IOException { + writer.beginObject(); + writer.name("labels").beginArray(); + for (String label : layout.labels) { + writer.value(label); + } + writer.endArray(); + StringBuilder types = new StringBuilder(layout.timestamps.length); + for (boolean ts : layout.timestamps) { + types.append(ts ? 'T' : 'S'); + } + writer.name("types").value(types.toString()); + writer.endObject(); + } + + private static void writeRow(@NotNull JsonWriter writer, @NotNull Object[] row) throws IOException { + writer.beginArray(); + for (Object value : row) { + if (value == null) { + writer.nullValue(); + } else if (value instanceof Long l) { + writer.value(l); + } else { + writer.value(value.toString()); + } + } + writer.endArray(); + } + + private class SaveJob extends AbstractJob { + SaveJob() { + super("Save metadata snapshot"); + setSystem(true); + setUser(false); + } + + @NotNull + @Override + protected IStatus run(@NotNull DBRProgressMonitor monitor) { + save(); + return Status.OK_STATUS; + } + } + + /////////////////////////////////////////////////////////////////////// + // Replay result set + + /** + * Read-only result set over stored rows. Implements the getters used by metadata readers + * (e.g. via {@code JDBCUtils.safeGet*}); everything else is reported as not supported, + * which makes the caller read the database instead. + */ + static final class RowReplay implements InvocationHandler { + private final Layout layout; + private final List rows; + private Object[] row; + private int position = -1; + private boolean wasNull; + + RowReplay(@NotNull Layout layout, @NotNull List rows) { + this.layout = layout; + this.rows = rows; + } + + @NotNull + ResultSet newResultSet() { + return (ResultSet) Proxy.newProxyInstance( + JDBCMetadataSnapshot.class.getClassLoader(), new Class[]{ResultSet.class}, this); + } + + @Nullable + @Override + public Object invoke(@NotNull Object proxy, @NotNull Method method, @Nullable Object[] args) throws Throwable { + String name = method.getName(); + switch (name) { + case "next" -> { + if (position + 1 >= rows.size()) { + row = null; + return false; + } + row = rows.get(++position); + return true; + } + case "isClosed" -> { + return false; + } + case "close", "clearWarnings", "getWarnings" -> { + return null; + } + case "wasNull" -> { + return wasNull; + } + case "findColumn" -> { + return columnIndex(args[0]) + 1; + } + case "hashCode" -> { + return System.identityHashCode(proxy); + } + case "equals" -> { + // Same replay, without calling equals() on the proxy itself + return args[0] != null && Proxy.isProxyClass(args[0].getClass()) + && Proxy.getInvocationHandler(args[0]).equals(this); + } + case "toString" -> { + return "Metadata snapshot rows"; + } + default -> { + if (name.startsWith("get") && args != null && args.length == 1) { + return getValue(name, columnIndex(args[0]), method.getReturnType()); + } + throw new SQLFeatureNotSupportedException(name + " is not supported by metadata snapshot"); + } + } + } + + private int columnIndex(@NotNull Object column) throws SQLException { + int index = column instanceof Number n ? n.intValue() - 1 : layout.indexOf(column.toString()); + if (index < 0 || index >= layout.labels.length) { + throw new SQLException("Column '" + column + "' not found"); + } + return index; + } + + @Nullable + private Object getValue(@NotNull String getter, int index, @NotNull Class type) throws SQLException { + if (row == null) { + throw new SQLException("Result set is not positioned on a row"); + } + Object value = row[index]; + wasNull = value == null; + switch (getter) { + case "getString", "getNString" -> { + return value == null ? null : layout.timestamps[index] ? new Timestamp((Long) value).toString() : value.toString(); + } + case "getTimestamp" -> { + return value instanceof Long l ? new Timestamp(l) : null; + } + case "getDate" -> { + return value instanceof Long l ? new Date(l) : null; + } + case "getObject" -> { + return value instanceof Long l && layout.timestamps[index] ? new Timestamp(l) : value; + } + case "getBoolean" -> { + if (value == null) { + return false; + } + String str = value.toString().trim(); + return isNumber(str) ? new BigDecimal(str).signum() != 0 : Boolean.parseBoolean(str); + } + case "getBigDecimal" -> { + return value == null ? null : toNumber(value); + } + case "getInt", "getLong", "getShort", "getByte", "getDouble", "getFloat" -> { + BigDecimal number = value == null ? BigDecimal.ZERO : toNumber(value); + if (type == int.class) { + return number.intValue(); + } + if (type == long.class) { + return number.longValue(); + } + if (type == short.class) { + return number.shortValue(); + } + if (type == byte.class) { + return number.byteValue(); + } + if (type == double.class) { + return number.doubleValue(); + } + return number.floatValue(); + } + default -> throw new SQLFeatureNotSupportedException(getter + " is not supported by metadata snapshot"); + } + } + + private static boolean isNumber(@NotNull String str) { + try { + new BigDecimal(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + @NotNull + private static BigDecimal toNumber(@NotNull Object value) throws SQLException { + if (value instanceof Long l) { + return BigDecimal.valueOf(l); + } + try { + return new BigDecimal(value.toString().trim()); + } catch (NumberFormatException e) { + throw new SQLException("Value '" + value + "' is not a number"); + } + } + } +} diff --git a/test/org.jkiss.dbeaver.model.jdbc.test/META-INF/MANIFEST.MF b/test/org.jkiss.dbeaver.model.jdbc.test/META-INF/MANIFEST.MF new file mode 100644 index 0000000000000..b23537a159868 --- /dev/null +++ b/test/org.jkiss.dbeaver.model.jdbc.test/META-INF/MANIFEST.MF @@ -0,0 +1,13 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: DBeaver JDBC Model Tests +Bundle-SymbolicName: org.jkiss.dbeaver.model.jdbc.test +Bundle-Version: 1.0.0.qualifier +Bundle-Release-Date: 20261102 +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Bundle-Vendor: DBeaver Corp +Fragment-Host: org.jkiss.dbeaver.model.jdbc +Bundle-ActivationPolicy: lazy +Require-Bundle: org.jkiss.dbeaver.test.platform, + org.jkiss.dbeaver.model, + org.jkiss.dbeaver.model.jdbc diff --git a/test/org.jkiss.dbeaver.model.jdbc.test/build.properties b/test/org.jkiss.dbeaver.model.jdbc.test/build.properties new file mode 100644 index 0000000000000..14fb1734d9f76 --- /dev/null +++ b/test/org.jkiss.dbeaver.model.jdbc.test/build.properties @@ -0,0 +1,5 @@ +source..=src/ +output..=target/classes/ +bin.includes=.,\ + META-INF/ +. diff --git a/test/org.jkiss.dbeaver.model.jdbc.test/pom.xml b/test/org.jkiss.dbeaver.model.jdbc.test/pom.xml new file mode 100644 index 0000000000000..c5823fc6c1077 --- /dev/null +++ b/test/org.jkiss.dbeaver.model.jdbc.test/pom.xml @@ -0,0 +1,32 @@ + + + + 4.0.0 + + org.jkiss.dbeaver + tests + 1.0.0-SNAPSHOT + ../ + + org.jkiss.dbeaver.model.jdbc.test + 1.0.0-SNAPSHOT + eclipse-test-plugin + + \ No newline at end of file diff --git a/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java b/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java new file mode 100644 index 0000000000000..363aa2e287349 --- /dev/null +++ b/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java @@ -0,0 +1,287 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed 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.jkiss.dbeaver.model.impl.jdbc.cache; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.exec.jdbc.JDBCSession; +import org.jkiss.dbeaver.model.impl.jdbc.JDBCDataSource; +import org.jkiss.dbeaver.model.impl.jdbc.JDBCExecutionContext; +import org.jkiss.dbeaver.model.impl.jdbc.exec.JDBCFactoryDefault; +import org.jkiss.junit.DBeaverUnitTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class JDBCMetadataSnapshotTest extends DBeaverUnitTest { + + private static final String TABLES = "tables"; + private static final String COLUMNS = "columns"; + private static final String SIGNATURE = "test"; + private static final Timestamp CREATED = Timestamp.valueOf("2026-01-02 03:04:05"); + + @TempDir + Path tempDir; + + private Path file; + private JDBCSession session; + + @BeforeEach + public void setUp() { + file = tempDir.resolve("snapshot.json.gz"); + JDBCDataSource dataSource = Mockito.mock(JDBCDataSource.class); + Mockito.when(dataSource.getJdbcFactory()).thenReturn(new JDBCFactoryDefault()); + session = Mockito.mock(JDBCSession.class); + Mockito.when(session.getDataSource()).thenReturn(dataSource); + Mockito.when(session.getExecutionContext()).thenReturn(Mockito.mock(JDBCExecutionContext.class)); + } + + @Test + public void restoresRowsWrittenToDisk() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + recordTables(snapshot, true, new Object[][]{{"T1", 10, CREATED}, {"T2", null, null}}); + snapshot.flush(); + + JDBCMetadataSnapshot restored = new JDBCMetadataSnapshot(file, SIGNATURE); + Assertions.assertTrue(restored.isComplete(TABLES)); + List rows = restored.restore(session, TABLES, null, rs -> { + String name = rs.getString("TABLE_NAME"); + int size = rs.getInt("ROW_COUNT"); + boolean sizeNull = rs.wasNull(); + Timestamp created = rs.getTimestamp("CREATED"); + return name + ":" + size + ":" + sizeNull + ":" + created; + }); + Assertions.assertEquals(List.of("T1:10:false:" + CREATED, "T2:0:true:null"), rows); + } + + @Test + public void ignoresSnapshotOfOtherSignature() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}}); + snapshot.flush(); + + JDBCMetadataSnapshot other = new JDBCMetadataSnapshot(file, "other"); + Assertions.assertFalse(other.isComplete(TABLES)); + Assertions.assertNull(other.restore(session, TABLES, null, rs -> rs.getString(1))); + } + + @Test + public void ignoresBrokenFile() throws Exception { + Files.writeString(file, "not a snapshot", StandardCharsets.UTF_8); + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + Assertions.assertFalse(snapshot.isComplete(TABLES)); + + recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}}); + snapshot.flush(); + Assertions.assertTrue(new JDBCMetadataSnapshot(file, SIGNATURE).isComplete(TABLES)); + } + + @Test + public void keepsSnapshotWhenPassIsNotCommitted() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}}); + + // Canceled load: rows are recorded, but the pass is not committed + JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(TABLES, true); + ResultSet resultSet = tableResultSet(new Object[][]{{"T2", 2, CREATED}}); + while (resultSet.next()) { + pass.record(resultSet.getString(1), resultSet); + } + Assertions.assertEquals(List.of("T1"), restoreNames(snapshot, null)); + } + + @Test + public void skipsPassWhenRowCannotBeRead() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}}); + + JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(TABLES, true); + ResultSet resultSet = tableResultSet(new Object[][]{{"T2", 2, CREATED}}); + resultSet.next(); + Mockito.when(resultSet.getString(2)).thenThrow(new SQLException("Unsupported type")); + pass.record("T2", resultSet); + pass.commit(); + Assertions.assertEquals(List.of("T1"), restoreNames(snapshot, null)); + } + + @Test + public void fullListReplacesGroups() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}, {"T2", 2, CREATED}}); + recordTables(snapshot, true, new Object[][]{{"T2", 2, CREATED}, {"T3", 3, CREATED}}); + Assertions.assertEquals(List.of("T2", "T3"), restoreNames(snapshot, null)); + } + + @Test + public void partialPassUpdatesGroups() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}, {"T2", 2, CREATED}}); + final long before = snapshot.getReadTimes(TABLES).get("T2"); + Thread.sleep(5); + recordTables(snapshot, false, new Object[][]{{"T2", 20, CREATED}}); + + Assertions.assertTrue(snapshot.isComplete(TABLES)); + Assertions.assertEquals(List.of("T2:20"), snapshot.restore(session, TABLES, "T2", + rs -> rs.getString("TABLE_NAME") + ":" + rs.getLong("ROW_COUNT"))); + Map readTimes = snapshot.getReadTimes(TABLES); + Assertions.assertTrue(readTimes.get("T2") > before); + Assertions.assertEquals(List.of("T1", "T2"), List.copyOf(readTimes.keySet())); + } + + @Test + public void restoresGroupsOfChildren() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(COLUMNS, false); + ResultSet resultSet = tableResultSet(new Object[][]{{"T1", 1, CREATED}, {"T1", 2, CREATED}}); + while (resultSet.next()) { + pass.record(resultSet.getString(1), resultSet); + } + pass.recordEmpty("T2"); + pass.commit(); + + Assertions.assertFalse(snapshot.isComplete(COLUMNS)); + Assertions.assertNull(snapshot.restore(session, COLUMNS, null, rs -> rs.getString(1))); + Assertions.assertEquals(List.of(1, 2), snapshot.restore(session, COLUMNS, "T1", rs -> rs.getInt(2))); + Assertions.assertEquals(List.of(), snapshot.restore(session, COLUMNS, "T2", rs -> rs.getInt(2))); + Assertions.assertNull(snapshot.restore(session, COLUMNS, "T3", rs -> rs.getInt(2))); + + snapshot.removeGroup(COLUMNS, "T1"); + Assertions.assertFalse(snapshot.hasGroup(COLUMNS, "T1")); + Assertions.assertTrue(snapshot.hasGroup(COLUMNS, "T2")); + } + + @Test + public void detectsChangedGroups() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}, {"T2", 2, CREATED}, {"T3", 3, CREATED}}); + + // T1 unchanged, T2 changed, T3 dropped, T4 created + JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(TABLES, true); + ResultSet resultSet = tableResultSet(new Object[][]{{"T1", 1, CREATED}, {"T2", 20, CREATED}, {"T4", 4, CREATED}}); + while (resultSet.next()) { + pass.record(resultSet.getString(1), resultSet); + } + Assertions.assertEquals(Set.of("T2", "T3", "T4"), pass.getChangedGroups()); + Assertions.assertTrue(pass.contains("T4")); + Assertions.assertFalse(pass.contains("T3")); + + // Partial pass: groups which were not read are not reported + JDBCMetadataSnapshot.Pass partial = snapshot.beginPass(TABLES, false); + resultSet = tableResultSet(new Object[][]{{"T1", 1, CREATED}}); + while (resultSet.next()) { + partial.record(resultSet.getString(1), resultSet); + } + partial.recordEmpty("T2"); + Assertions.assertEquals(Set.of("T2"), partial.getChangedGroups()); + } + + @Test + public void restoresAllGroups() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(COLUMNS, true); + ResultSet resultSet = tableResultSet(new Object[][]{{"T1", 1, CREATED}, {"T2", 2, CREATED}, {"T1", 3, CREATED}}); + while (resultSet.next()) { + pass.record(resultSet.getString(1), resultSet); + } + pass.recordEmpty("T3"); + pass.commit(); + + Map> restored = snapshot.restoreGroups(session, COLUMNS, + (group, rs) -> group + ":" + rs.getInt("ROW_COUNT")); + Assertions.assertEquals(Map.of("T1", List.of("T1:1", "T1:3"), "T2", List.of("T2:2"), "T3", List.of()), restored); + Assertions.assertNull(snapshot.restoreGroups(session, TABLES, (group, rs) -> group)); + } + + @Test + public void clearForgetsSet() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}}); + snapshot.clear(TABLES); + snapshot.flush(); + Assertions.assertFalse(new JDBCMetadataSnapshot(file, SIGNATURE).isComplete(TABLES)); + } + + @Test + public void reportsUnsupportedGetters() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}}); + Assertions.assertThrows(SQLFeatureNotSupportedException.class, + () -> snapshot.restore(session, TABLES, null, rs -> rs.getBytes(1))); + Assertions.assertThrows(SQLException.class, + () -> snapshot.restore(session, TABLES, null, rs -> rs.getString("MISSING"))); + } + + @NotNull + private List restoreNames(@NotNull JDBCMetadataSnapshot snapshot, @Nullable String group) throws Exception { + return snapshot.restore(session, TABLES, group, rs -> rs.getString("TABLE_NAME")); + } + + private static void recordTables( + @NotNull JDBCMetadataSnapshot snapshot, + boolean fullList, + @NotNull Object[][] rows + ) throws SQLException { + JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(TABLES, fullList); + ResultSet resultSet = tableResultSet(rows); + while (resultSet.next()) { + pass.record(resultSet.getString(1), resultSet); + } + pass.commit(); + } + + /** + * Result set with columns TABLE_NAME (varchar), ROW_COUNT (integer) and CREATED (timestamp). + */ + @NotNull + private static ResultSet tableResultSet(@NotNull Object[][] rows) throws SQLException { + ResultSetMetaData meta = Mockito.mock(ResultSetMetaData.class); + Mockito.when(meta.getColumnCount()).thenReturn(3); + Mockito.when(meta.getColumnLabel(1)).thenReturn("TABLE_NAME"); + Mockito.when(meta.getColumnLabel(2)).thenReturn("ROW_COUNT"); + Mockito.when(meta.getColumnLabel(3)).thenReturn("CREATED"); + Mockito.when(meta.getColumnType(1)).thenReturn(Types.VARCHAR); + Mockito.when(meta.getColumnType(2)).thenReturn(Types.INTEGER); + Mockito.when(meta.getColumnType(3)).thenReturn(Types.TIMESTAMP); + + ResultSet resultSet = Mockito.mock(ResultSet.class); + int[] position = {-1}; + Mockito.when(resultSet.getMetaData()).thenReturn(meta); + Mockito.when(resultSet.next()).thenAnswer(inv -> ++position[0] < rows.length); + Mockito.when(resultSet.getString(Mockito.anyInt())).thenAnswer(inv -> { + Object value = rows[position[0]][(int) inv.getArgument(0) - 1]; + return value == null ? null : value.toString(); + }); + Mockito.when(resultSet.getTimestamp(Mockito.anyInt())).thenAnswer( + inv -> (Timestamp) rows[position[0]][(int) inv.getArgument(0) - 1]); + return resultSet; + } +} diff --git a/test/pom.xml b/test/pom.xml index 2725533228a7d..161ea4f112413 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -37,6 +37,7 @@ org.jkiss.dbeaver.model.ai.test org.jkiss.dbeaver.net.ssh.sshj.test org.jkiss.dbeaver.model.sql.test + org.jkiss.dbeaver.model.jdbc.test org.jkiss.dbeaver.data.transfer.test From 9d3bd1f2f4add78336035c85994d87f409ba4bef Mon Sep 17 00:00:00 2001 From: Isabella Tessarotto Date: Sun, 18 Oct 2026 11:27:28 -0300 Subject: [PATCH 3/7] dbeaver/dbeaver#42474 Keep JDBC cache metadata between sessions Assisted by: Claude Code --- .../dbeaver/ext/generic/model/TableCache.java | 8 +- .../impl/jdbc/cache/JDBCObjectCache.java | 356 +++++++++++++++--- .../impl/jdbc/cache/JDBCStructCache.java | 292 ++++++++++++-- .../org/jkiss/dbeaver/ModelPreferences.java | 3 + .../ui/internal/UIConnectionMessages.java | 2 + .../internal/UIConnectionMessages.properties | 2 + .../UIConnectionMessages_pt_BR.properties | 2 + .../ui/preferences/PrefPageMetaData.java | 14 +- 8 files changed, 596 insertions(+), 83 deletions(-) diff --git a/plugins/org.jkiss.dbeaver.ext.generic/src/org/jkiss/dbeaver/ext/generic/model/TableCache.java b/plugins/org.jkiss.dbeaver.ext.generic/src/org/jkiss/dbeaver/ext/generic/model/TableCache.java index c153a7104c4bd..fdcd9bffdd2d0 100644 --- a/plugins/org.jkiss.dbeaver.ext.generic/src/org/jkiss/dbeaver/ext/generic/model/TableCache.java +++ b/plugins/org.jkiss.dbeaver.ext.generic/src/org/jkiss/dbeaver/ext/generic/model/TableCache.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,6 +56,12 @@ public GenericMetaObject getTableObject() { return tableObject; } + @Override + protected boolean supportsMetadataSnapshot() { + // Tables and columns are built from the rows of the metadata queries only + return true; + } + @NotNull @Override public JDBCStatement prepareLookupStatement( diff --git a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCObjectCache.java b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCObjectCache.java index 6fb15e3683a32..807346be74620 100644 --- a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCObjectCache.java +++ b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCObjectCache.java @@ -16,18 +16,23 @@ */ package org.jkiss.dbeaver.model.impl.jdbc.cache; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBDatabaseException; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.ModelPreferences; import org.jkiss.dbeaver.model.DBConstants; import org.jkiss.dbeaver.model.DBPDataSource; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.exec.jdbc.JDBCResultSet; import org.jkiss.dbeaver.model.exec.jdbc.JDBCSession; import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement; import org.jkiss.dbeaver.model.messages.ModelMessages; +import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.struct.DBSObject; import org.jkiss.dbeaver.model.struct.cache.AbstractObjectCache; @@ -36,8 +41,10 @@ import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Set; /** * Various objects cache. @@ -49,9 +56,20 @@ public abstract class JDBCObjectCache tmpObjectList = new ArrayList<>(); - DBPDataSource dataSource = owner.getDataSource(); if (dataSource == null) { throw new DBException(ModelMessages.error_not_connected_to_database); } + JDBCMetadataSnapshot snapshot = getMetadataSnapshot(owner); + if (snapshot != null && snapshotRestoreAllowed) { + // Only the first load of the session: after an explicit refresh objects are read from the database + snapshotRestoreAllowed = false; + List restored = restoreObjects(monitor, owner, snapshot); + if (restored != null) { + finishLoading(monitor, owner, restored); + objectsRefreshPending = true; + scheduleSnapshotRefresh(owner, snapshot); + return; + } + } + + List tmpObjectList = new ArrayList<>(); if (owner.isPersisted()) { // Load cache from database only for persisted objects try { - try (JDBCSession session = DBUtils.openMetaSession(monitor, owner, "Load objects from " + DBUtils.getObjectTypeName(owner) + " '" + owner.getName() + "'")) { - beforeCacheLoading(session, owner); - try (JDBCStatement dbStat = prepareObjectsStatement(session, owner)) { - monitor.subTask("Load " + getCacheName()); - dbStat.setFetchSize(DBConstants.METADATA_FETCH_SIZE); - dbStat.executeStatement(); - try (JDBCResultSet dbResult = dbStat.getResultSet()) { - if (dbResult != null) { - while (dbResult.next()) { - if (monitor.isCanceled()) { - return; - } - - OBJECT object = fetchObject(session, owner, dbResult); - if (object == null || !isValidObject(monitor, owner, object)) { - continue; - } - tmpObjectList.add(object); - - // Do not log every object load. This overheats UI in case of long lists - //monitor.subTask(object.getName()); - if (tmpObjectList.size() == maximumCacheSize) { - log.warn("Maximum cache size exceeded (" + maximumCacheSize + ") in " + this); - break; - } - } - } - } - } finally { - afterCacheLoading(session, owner); - } - } catch (SQLException ex) { - throw new DBDatabaseException(ex, dataSource); - } catch (DBException ex) { - throw ex; - } catch (Exception ex) { - throw new DBException("Internal driver error", ex); + JDBCMetadataSnapshot.Pass pass = snapshot == null ? null : snapshot.beginPass(SNAPSHOT_OBJECTS, true); + if (!readObjects(monitor, owner, tmpObjectList, pass)) { + return; + } + if (pass != null) { + pass.commit(); } } catch (Exception e) { if (!handleCacheReadError(e)) { @@ -152,20 +151,257 @@ protected synchronized void loadObjects(DBRProgressMonitor monitor, OWNER owner) } } } + finishLoading(monitor, owner, tmpObjectList); + } + + /** + * Reads objects from the database, without changing the cache. + * + * @param pass records the rows read in the metadata snapshot, if any + * @return false if the read was canceled + */ + private boolean readObjects( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @NotNull List objects, + @Nullable JDBCMetadataSnapshot.Pass pass + ) throws DBException { + DBPDataSource dataSource = owner.getDataSource(); + String task = "Load objects from " + DBUtils.getObjectTypeName(owner) + " '" + owner.getName() + "'"; + try (JDBCSession session = DBUtils.openMetaSession(monitor, owner, task)) { + beforeCacheLoading(session, owner); + try (JDBCStatement dbStat = prepareObjectsStatement(session, owner)) { + monitor.subTask("Load " + getCacheName()); + dbStat.setFetchSize(DBConstants.METADATA_FETCH_SIZE); + dbStat.executeStatement(); + try (JDBCResultSet dbResult = dbStat.getResultSet()) { + if (dbResult != null) { + while (dbResult.next()) { + if (monitor.isCanceled()) { + return false; + } - addCustomObjects(monitor, owner, tmpObjectList); + OBJECT object = fetchObject(session, owner, dbResult); + if (object == null || !isValidObject(monitor, owner, object)) { + continue; + } + objects.add(object); + if (pass != null) { + pass.record(object.getName(), dbResult); + } + + // Do not log every object load. This overheats UI in case of long lists + //monitor.subTask(object.getName()); + if (objects.size() == maximumCacheSize) { + log.warn("Maximum cache size exceeded (" + maximumCacheSize + ") in " + this); + break; + } + } + } + } + } finally { + afterCacheLoading(session, owner); + } + } catch (SQLException ex) { + throw new DBDatabaseException(ex, dataSource); + } catch (DBException ex) { + throw ex; + } catch (Exception ex) { + throw new DBException("Internal driver error", ex); + } + return true; + } + + private void finishLoading( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @NotNull List objects + ) throws DBException { + addCustomObjects(monitor, owner, objects); Comparator comparator = getListOrderComparator(); - if (comparator != null && !CommonUtils.isEmpty(tmpObjectList)) { - tmpObjectList.sort(comparator); + if (comparator != null && !CommonUtils.isEmpty(objects)) { + objects.sort(comparator); } detectCaseSensitivity(owner); - mergeCache(tmpObjectList); + mergeCache(objects); this.invalidateObjects(monitor, owner, new CacheIterator()); } - public void beforeCacheLoading(@NotNull JDBCSession session, OWNER owner) throws DBException { + @Override + public void clearCache() { + super.clearCache(); + // Explicit refresh: objects are read from the database (and stored in the snapshot again) + snapshotRestoreAllowed = false; + } + + /** + * Whether rows of this cache may be kept in a metadata snapshot between sessions (if the snapshot is enabled + * for the connection). Only for caches whose objects are built from the rows of the cache query alone: + * restored objects are built by {@link #fetchObject} from stored rows. + */ + protected boolean supportsMetadataSnapshot() { + return false; + } + + /** + * Driver settings which change the rows read by this cache (e.g. options used in its queries). + * A snapshot written with other settings is not restored. + */ + @NotNull + protected String[] getMetadataSnapshotSettings(@NotNull OWNER owner) { + return new String[0]; + } + + /** + * Local copy of the rows read by this cache, or null if metadata snapshots are not used. + */ + @Nullable + protected JDBCMetadataSnapshot getMetadataSnapshot(@NotNull OWNER owner) { + if (!supportsMetadataSnapshot() || !owner.isPersisted()) { + return null; + } + DBPDataSource dataSource = owner.getDataSource(); + if (dataSource == null) { + return null; + } + DBPDataSourceContainer container = dataSource.getContainer(); + if (!container.getPreferenceStore().getBoolean(ModelPreferences.META_SNAPSHOT_ENABLED)) { + return null; + } + synchronized (snapshotSync) { + if (metadataSnapshot == null) { + String ownerId = getClass().getName() + "/" + DBUtils.getObjectFullId(owner); + metadataSnapshot = new JDBCMetadataSnapshot( + JDBCMetadataSnapshot.getSnapshotFile(container, ownerId), + JDBCMetadataSnapshot.makeSignature(container, getMetadataSnapshotSettings(owner))); + } + return metadataSnapshot; + } + } + + @Nullable + private List restoreObjects( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @NotNull JDBCMetadataSnapshot snapshot + ) { + if (!snapshot.isComplete(SNAPSHOT_OBJECTS)) { + return null; + } + try (JDBCSession session = DBUtils.openMetaSession(monitor, owner, "Restore metadata of '" + owner.getName() + "'")) { + beforeCacheLoading(session, owner); + try { + List objects = snapshot.restore(session, SNAPSHOT_OBJECTS, null, rs -> fetchObject(session, owner, rs)); + if (objects == null) { + return null; + } + List result = new ArrayList<>(objects.size()); + for (OBJECT object : objects) { + if (isValidObject(monitor, owner, object)) { + result.add(object); + } + } + log.debug("Restored " + result.size() + " objects of " + getCacheName() + " from metadata snapshot"); + return result; + } finally { + afterCacheLoading(session, owner); + } + } catch (Exception e) { + // E.g. the cache reads a column type which the snapshot cannot replay + log.debug("Metadata snapshot of " + getCacheName() + " not restored, reading from database: " + e.getMessage()); + snapshot.clear(SNAPSHOT_OBJECTS); + return null; + } + } + + /** + * Schedules a background refresh of the metadata restored from the snapshot, unless it is already scheduled. + */ + protected void scheduleSnapshotRefresh(@NotNull OWNER owner, @NotNull JDBCMetadataSnapshot snapshot) { + synchronized (snapshotSync) { + if (snapshotRefreshScheduled) { + return; + } + snapshotRefreshScheduled = true; + } + new SnapshotRefreshJob(owner, snapshot).schedule(SNAPSHOT_REFRESH_DELAY_MS); + } + + /** + * Whether some metadata was restored from the snapshot and not refreshed yet. + */ + protected boolean isSnapshotRefreshPending() { + return objectsRefreshPending; + } + + /** + * Reads objects from the database and updates restored objects which changed since the snapshot was taken. + * The database is read without the cache lock, so the cache stays usable meanwhile. + */ + protected void refreshRestoredMetadata( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @NotNull JDBCMetadataSnapshot snapshot + ) throws DBException { + if (!objectsRefreshPending) { + return; + } + objectsRefreshPending = false; + if (!isFullyCached()) { + // Cleared meanwhile: the next load reads the database + return; + } + JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(SNAPSHOT_OBJECTS, true); + List objects = new ArrayList<>(); + if (!readObjects(monitor, owner, objects, pass)) { + return; + } + Set changed = pass.getChangedGroups(); + List added = new ArrayList<>(); + List updated = new ArrayList<>(); + List removed = new ArrayList<>(); + synchronized (this) { + if (!isFullyCached()) { + return; + } + for (OBJECT object : objects) { + if (!changed.contains(object.getName())) { + continue; + } + OBJECT oldObject = getCachedObject(object.getName()); + if (oldObject != null && oldObject.getClass() == object.getClass()) { + // Cached instance may be referenced by navigator nodes and editors + deepCopyCachedObject(object, oldObject); + updated.add(oldObject); + } else { + if (oldObject != null) { + removeObject(oldObject, false); + removed.add(oldObject); + } + cacheObject(object); + added.add(object); + } + } + for (String name : changed) { + OBJECT oldObject = pass.contains(name) ? null : getCachedObject(name); + if (oldObject != null) { + removeObject(oldObject, false); + removed.add(oldObject); + } + } + pass.commit(); + } + removed.forEach(DBUtils::fireObjectRemove); + added.forEach(object -> DBUtils.fireObjectAdd(object, Collections.emptyMap())); + updated.forEach(DBUtils::fireObjectUpdate); + if (!changed.isEmpty()) { + log.debug("Metadata of " + getCacheName() + " refreshed: " + changed.size() + " objects changed"); + } + } + + public void beforeCacheLoading(@NotNull JDBCSession session, @NotNull OWNER owner) throws DBException { // Do nothing } @@ -183,4 +419,36 @@ protected boolean handleCacheReadError(@NotNull Exception error) { return false; } + private class SnapshotRefreshJob extends AbstractJob { + private final OWNER owner; + private final JDBCMetadataSnapshot snapshot; + + SnapshotRefreshJob(@NotNull OWNER owner, @NotNull JDBCMetadataSnapshot snapshot) { + super("Refresh metadata of '" + owner.getName() + "'"); + this.owner = owner; + this.snapshot = snapshot; + setSystem(true); + setUser(false); + } + + @NotNull + @Override + protected IStatus run(@NotNull DBRProgressMonitor monitor) { + try { + refreshRestoredMetadata(monitor, owner, snapshot); + } catch (Exception e) { + // The snapshot stays as it is: the next session reads it again + log.debug("Error refreshing metadata of '" + owner.getName() + "': " + e.getMessage()); + } finally { + synchronized (snapshotSync) { + snapshotRefreshScheduled = false; + } + } + if (isSnapshotRefreshPending() && !monitor.isCanceled()) { + // Restored meanwhile + scheduleSnapshotRefresh(owner, snapshot); + } + return Status.OK_STATUS; + } + } } diff --git a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCStructCache.java b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCStructCache.java index 41b34438c735c..8920fb96abcfb 100644 --- a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCStructCache.java +++ b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCStructCache.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,17 +47,31 @@ public abstract class JDBCStructCache { private static final Log log = Log.getLog(JDBCStructCache.class); + private static final String SNAPSHOT_CHILDREN = "children"; + private final Object objectNameColumn; private volatile boolean childrenCached = false; + private volatile boolean childrenRestoreAllowed = true; + // Objects whose children came from the metadata snapshot: they are refreshed in background + private final Set restoredChildren = new HashSet<>(); private final Map> childrenCache = new IdentityHashMap<>(); - abstract protected JDBCStatement prepareChildrenStatement(@NotNull JDBCSession session, @NotNull OWNER owner, @Nullable OBJECT forObject) - throws SQLException; + @NotNull + protected abstract JDBCStatement prepareChildrenStatement( + @NotNull JDBCSession session, + @NotNull OWNER owner, + @Nullable OBJECT forObject + ) throws SQLException; - abstract protected CHILD fetchChild(@NotNull JDBCSession session, @NotNull OWNER owner, @NotNull OBJECT parent, @NotNull JDBCResultSet dbResult) - throws SQLException, DBException; + @Nullable + protected abstract CHILD fetchChild( + @NotNull JDBCSession session, + @NotNull OWNER owner, + @NotNull OBJECT parent, + @NotNull JDBCResultSet dbResult + ) throws SQLException, DBException; - protected JDBCStructCache(Object objectNameColumn) { + protected JDBCStructCache(@NotNull Object objectNameColumn) { this.objectNameColumn = objectNameColumn; } @@ -84,13 +98,68 @@ public synchronized void loadChildren(DBRProgressMonitor monitor, OWNER owner, @ super.loadObjects(monitor, owner); } + JDBCMetadataSnapshot snapshot = getMetadataSnapshot(owner); + if (snapshot != null && childrenRestoreAllowed && restoreChildren(monitor, owner, forObject, snapshot)) { + scheduleSnapshotRefresh(owner, snapshot); + return; + } + JDBCMetadataSnapshot.Pass pass = snapshot == null ? null : snapshot.beginPass(SNAPSHOT_CHILDREN, forObject == null); + Map> objectMap = readChildren(monitor, owner, forObject, pass, true); + if (objectMap == null) { + return; + } + + // All children are read. Now assign them to parents + for (Map.Entry> colEntry : objectMap.entrySet()) { + if (!isChildrenCached(colEntry.getKey())) { + // isChildrenCached may return true if the same cache was read in other thread + // just skip + cacheChildren(colEntry.getKey(), colEntry.getValue()); + } + } + if (forObject == null) { + if (objectMap.isEmpty()) { + // Nothing was read. May be it means empty list of children + // but possibly this feature is not supported [JDBC: SQLite] + return; + } else { + // Now set empty column list for other tables + for (OBJECT tmpObject : getAllObjects(monitor, owner)) { + if (!isChildrenCached(tmpObject) && !objectMap.containsKey(tmpObject)) { + cacheChildren(tmpObject, new ArrayList<>()); + } + } + this.childrenCached = true; + } + } else if (!objectMap.containsKey(forObject)) { + cacheChildren(forObject, new ArrayList<>()); + } + if (pass != null) { + pass.commit(); + } + } + + /** + * Reads children from the database, without changing the cache. + * + * @param pass records the rows read in the metadata snapshot, if any + * @param skipCached skip children of objects which are already cached + * @return children by parent, or null if the read was canceled + */ + @Nullable + private Map> readChildren( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @Nullable OBJECT forObject, + @Nullable JDBCMetadataSnapshot.Pass pass, + boolean skipCached + ) throws DBException { DBPDataSource dataSource = owner.getDataSource(); if (dataSource == null) { throw new DBException(ModelMessages.error_not_connected_to_database); } + Map> objectMap = new HashMap<>(); try (JDBCSession session = DBUtils.openMetaSession(monitor, owner, "Load child objects")) { - Map> objectMap = new HashMap<>(); - // Load columns try (JDBCStatement dbStat = prepareChildrenStatement(session, owner, forObject)) { dbStat.setFetchSize(DBConstants.METADATA_FETCH_SIZE); @@ -100,7 +169,7 @@ public synchronized void loadChildren(DBRProgressMonitor monitor, OWNER owner, @ try { while (dbResult.next()) { if (monitor.isCanceled()) { - return; + return null; } OBJECT object = forObject; if (object == null) { @@ -121,7 +190,11 @@ public synchronized void loadChildren(DBRProgressMonitor monitor, OWNER owner, @ continue; } } - if (isChildrenCached(object)) { + if (pass != null) { + // Recorded even if already cached: the snapshot keeps all rows of the read + pass.record(object.getName(), dbResult); + } + if (skipCached && isChildrenCached(object)) { // Already read continue; } @@ -136,32 +209,7 @@ public synchronized void loadChildren(DBRProgressMonitor monitor, OWNER owner, @ } if (monitor.isCanceled()) { - return; - } - - // All children are read. Now assign them to parents - for (Map.Entry> colEntry : objectMap.entrySet()) { - if (!isChildrenCached(colEntry.getKey())) { - // isChildrenCached may return true if the same cache was read in other thread - // just skip - cacheChildren(colEntry.getKey(), colEntry.getValue()); - } - } - if (forObject == null) { - if (objectMap.isEmpty()) { - // Nothing was read. May be it means empty list of children - // but possibly this feature is not supported [JDBC: SQLite] - } else { - // Now set empty column list for other tables - for (OBJECT tmpObject : getAllObjects(monitor, owner)) { - if (!isChildrenCached(tmpObject) && !objectMap.containsKey(tmpObject)) { - cacheChildren(tmpObject, new ArrayList<>()); - } - } - this.childrenCached = true; - } - } else if (!objectMap.containsKey(forObject)) { - cacheChildren(forObject, new ArrayList<>()); + return null; } } finally { dbResult.close(); @@ -171,6 +219,174 @@ public synchronized void loadChildren(DBRProgressMonitor monitor, OWNER owner, @ } catch (SQLException ex) { throw new DBDatabaseException(ex, dataSource); } + if (pass != null) { + // Objects without children + if (forObject != null) { + pass.recordEmpty(forObject.getName()); + } else { + for (OBJECT object : getCachedObjects()) { + pass.recordEmpty(object.getName()); + } + } + } + return objectMap; + } + + /** + * Restores children of one object (or of all objects) from the snapshot. + * + * @return false if they are not stored or cannot be restored + */ + private boolean restoreChildren( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @Nullable OBJECT forObject, + @NotNull JDBCMetadataSnapshot snapshot + ) { + if (forObject != null ? !snapshot.hasGroup(SNAPSHOT_CHILDREN, forObject.getName()) : !snapshot.isComplete(SNAPSHOT_CHILDREN)) { + return false; + } + try (JDBCSession session = DBUtils.openMetaSession(monitor, owner, "Restore metadata of '" + owner.getName() + "'")) { + if (forObject != null) { + List children = snapshot.restore(session, SNAPSHOT_CHILDREN, forObject.getName(), + rs -> fetchChild(session, owner, forObject, rs)); + if (children == null) { + return false; + } + cacheChildren(forObject, children); + restoredChildren.add(forObject.getName()); + return true; + } + List objects = getCachedObjects(); + for (OBJECT object : objects) { + if (!isChildrenCached(object) && !snapshot.hasGroup(SNAPSHOT_CHILDREN, object.getName())) { + // Object created after the snapshot was taken: read all children from the database + return false; + } + } + Map> restored = snapshot.restoreGroups(session, SNAPSHOT_CHILDREN, (group, rs) -> { + OBJECT parent = getCachedObject(group); + return parent == null ? null : fetchChild(session, owner, parent, rs); + }); + if (restored == null) { + return false; + } + for (OBJECT object : objects) { + List children = restored.get(object.getName()); + if (!isChildrenCached(object) && children != null) { + cacheChildren(object, children); + restoredChildren.add(object.getName()); + } + } + this.childrenCached = true; + log.debug("Restored children of " + restored.size() + " objects of " + getCacheName() + " from metadata snapshot"); + return true; + } catch (Exception e) { + log.debug("Metadata snapshot of " + getCacheName() + " children not restored, reading from database: " + e.getMessage()); + snapshot.clear(SNAPSHOT_CHILDREN); + return false; + } + } + + @Override + protected boolean isSnapshotRefreshPending() { + synchronized (this) { + return super.isSnapshotRefreshPending() || !restoredChildren.isEmpty(); + } + } + + @Override + protected void refreshRestoredMetadata( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @NotNull JDBCMetadataSnapshot snapshot + ) throws DBException { + super.refreshRestoredMetadata(monitor, owner, snapshot); + Set restored; + synchronized (this) { + restored = new LinkedHashSet<>(restoredChildren); + restoredChildren.clear(); + } + if (restored.isEmpty() || monitor.isCanceled()) { + return; + } + // Children of all objects are read in one query, if all of them are stored. Otherwise, object by object. + boolean allObjects = snapshot.isComplete(SNAPSHOT_CHILDREN); + JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(SNAPSHOT_CHILDREN, allObjects); + Map> children = new HashMap<>(); + if (allObjects) { + Map> objectMap = readChildren(monitor, owner, null, pass, false); + if (objectMap == null) { + return; + } + objectMap.forEach((object, list) -> children.put(object.getName(), list)); + } else { + for (String name : restored) { + OBJECT object = getCachedObject(name); + if (object == null) { + continue; + } + Map> objectMap = readChildren(monitor, owner, object, pass, false); + if (objectMap == null) { + return; + } + children.put(name, objectMap.getOrDefault(object, new ArrayList<>())); + } + } + List added = new ArrayList<>(); + List updated = new ArrayList<>(); + List removed = new ArrayList<>(); + int changedObjects = 0; + synchronized (this) { + for (String name : pass.getChangedGroups()) { + OBJECT object = restored.contains(name) ? getCachedObject(name) : null; + if (object != null && isChildrenCached(object)) { + mergeChildren(object, children.getOrDefault(name, new ArrayList<>()), added, updated, removed); + changedObjects++; + } + } + pass.commit(); + } + removed.forEach(DBUtils::fireObjectRemove); + added.forEach(child -> DBUtils.fireObjectAdd(child, Collections.emptyMap())); + updated.forEach(DBUtils::fireObjectUpdate); + if (changedObjects > 0) { + log.debug("Children of " + changedObjects + " objects of " + getCacheName() + " refreshed"); + } + } + + /** + * Replaces cached children of an object with children read again. Children with the same name keep their + * cached instance (which may be referenced by navigator nodes and editors), with the new content copied into it. + */ + private void mergeChildren( + @NotNull OBJECT object, + @NotNull List newChildren, + @NotNull List added, + @NotNull List updated, + @NotNull List removed + ) { + Map oldChildren = new LinkedHashMap<>(); + for (CHILD child : getChildrenCache(object).getCachedObjects()) { + oldChildren.put(child.getName(), child); + } + List merged = new ArrayList<>(newChildren.size()); + for (CHILD child : newChildren) { + CHILD oldChild = oldChildren.remove(child.getName()); + if (oldChild != null && oldChild.getClass() == child.getClass()) { + deepCopyCachedObject(child, oldChild); + merged.add(oldChild); + updated.add(oldChild); + } else { + if (oldChild != null) { + removed.add(oldChild); + } + merged.add(child); + added.add(child); + } + } + removed.addAll(oldChildren.values()); + cacheChildren(object, merged); } @Override @@ -228,7 +444,9 @@ public CHILD getChild(@Nullable DBRProgressMonitor monitor, @NotNull OWNER owner } } - public void clearChildrenCache(OBJECT forParent) { + public void clearChildrenCache(@Nullable OBJECT forParent) { + // Explicit refresh: children are read from the database + childrenRestoreAllowed = false; synchronized (childrenCache) { if (forParent != null) { this.childrenCache.remove(forParent); diff --git a/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/ModelPreferences.java b/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/ModelPreferences.java index 6df651ed28d29..0efb4db969f6b 100644 --- a/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/ModelPreferences.java +++ b/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/ModelPreferences.java @@ -206,6 +206,8 @@ public String toString() { public static final String META_CASE_SENSITIVE = "database.meta.casesensitive"; //$NON-NLS-1$ public static final String META_USE_SERVER_SIDE_FILTERS = "database.meta.server.side.filters"; //$NON-NLS-1$ public static final String META_EXTRA_DDL_INFO = "database.meta.extra.ddl.info"; //$NON-NLS-1$ + // Metadata rows are kept in the workspace between sessions (see JDBCMetadataSnapshot) + public static final String META_SNAPSHOT_ENABLED = "database.meta.snapshot.enabled"; //$NON-NLS-1$ public static final String META_CLIENT_NAME_DISABLE = "database.meta.client.name.disable"; //$NON-NLS-1$ public static final String META_CLIENT_NAME_OVERRIDE = "database.meta.client.name.override"; //$NON-NLS-1$ @@ -336,6 +338,7 @@ private static void initializeDefaultPreferences(DBPPreferenceStore store) { PrefUtils.setDefaultPreferenceValue(store, META_DISABLE_EXTRA_READ, false); PrefUtils.setDefaultPreferenceValue(store, META_EXTRA_DDL_INFO, true); PrefUtils.setDefaultPreferenceValue(store, META_USE_SERVER_SIDE_FILTERS, true); + PrefUtils.setDefaultPreferenceValue(store, META_SNAPSHOT_ENABLED, false); PrefUtils.setDefaultPreferenceValue(store, META_CLIENT_NAME_DISABLE, false); PrefUtils.setDefaultPreferenceValue(store, META_CLIENT_NAME_OVERRIDE, false); diff --git a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.java b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.java index dbb93b9329730..6638b438d3012 100644 --- a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.java +++ b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.java @@ -519,6 +519,8 @@ public class UIConnectionMessages extends NLS { public static String pref_page_database_general_checkbox_rollback_on_error; public static String pref_page_database_general_checkbox_show_row_count; public static String pref_page_database_general_checkbox_show_row_count_tip; + public static String pref_page_database_general_checkbox_metadata_snapshot; + public static String pref_page_database_general_checkbox_metadata_snapshot_tip; public static String pref_page_database_general_group_metadata; public static String pref_page_database_general_group_performance; public static String pref_page_database_general_separate_meta_connection; diff --git a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.properties b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.properties index f8bc326a3b128..d835731b88e62 100644 --- a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.properties +++ b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.properties @@ -501,6 +501,8 @@ pref_page_database_general_checkbox_disable_extra_metadata_tip = Do not read dat pref_page_database_general_checkbox_rollback_on_error = Rollback on error pref_page_database_general_checkbox_show_row_count = Show row count for tables pref_page_database_general_checkbox_show_row_count_tip = Disable this option if your database executes row count queries too slowly (e.g. because of very large tables) +pref_page_database_general_checkbox_metadata_snapshot = Keep metadata between sessions +pref_page_database_general_checkbox_metadata_snapshot_tip = Saves tables and their columns in the workspace and reuses them on the next connection, so navigator and SQL completion do not wait for the database. They are read again in background and updated if they changed. Metadata is stored on this computer. Supported by some database drivers only. pref_page_database_general_group_metadata = Metadata pref_page_database_general_group_performance = Performance pref_page_database_general_separate_meta_connection = Open separate connection for metadata read diff --git a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages_pt_BR.properties b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages_pt_BR.properties index 0c202b3883521..0fc8c9ff70e67 100644 --- a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages_pt_BR.properties +++ b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages_pt_BR.properties @@ -679,6 +679,8 @@ pref_page_database_general_checkbox_disable_extra_metadata_tip = Não ler os met pref_page_database_general_checkbox_rollback_on_error = Efetuar rollback em caso de erro pref_page_database_general_checkbox_show_row_count = Exibir número de linhas para tabelas pref_page_database_general_checkbox_show_row_count_tip = Desabilite esta opção se seu banco de dados executa consultas com contagem de linhas muito devagar (por exemplo, em tabelas muito grandes) +pref_page_database_general_checkbox_metadata_snapshot = Manter metadados entre sessões +pref_page_database_general_checkbox_metadata_snapshot_tip = Salva as tabelas e suas colunas no workspace e as reutiliza na próxima conexão, para que o navegador e o autocompletar de SQL não esperem pelo banco de dados. Elas são lidas novamente em segundo plano e atualizadas se mudaram. Os metadados ficam armazenados neste computador. Suportado apenas por alguns drivers. pref_page_database_general_group_metadata = Metadados pref_page_database_general_group_performance = Performance pref_page_database_general_separate_meta_connection = Abrir uma conexão separada para leitura de metadados diff --git a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/preferences/PrefPageMetaData.java b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/preferences/PrefPageMetaData.java index e02d0f2952f3d..4e50dc701881b 100644 --- a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/preferences/PrefPageMetaData.java +++ b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/preferences/PrefPageMetaData.java @@ -44,6 +44,7 @@ public class PrefPageMetaData extends TargetPrefPage public static final String PAGE_ID = "org.jkiss.dbeaver.preferences.main.meta"; //$NON-NLS-1$ private Button readExpensiveCheck; + private Button metadataSnapshotCheck; private Combo separateMetaConnectionCombo; private Button disableExtraMetadataRead; private Button caseSensitiveNamesCheck; @@ -64,7 +65,8 @@ protected boolean hasDataSourceSpecificOptions(DBPDataSourceContainer dataSource store.contains(ModelPreferences.META_SEPARATE_CONNECTION) || store.contains(ModelPreferences.META_CASE_SENSITIVE) || store.contains(ModelPreferences.META_DISABLE_EXTRA_READ) || - store.contains(ModelPreferences.META_USE_SERVER_SIDE_FILTERS) + store.contains(ModelPreferences.META_USE_SERVER_SIDE_FILTERS) || + store.contains(ModelPreferences.META_SNAPSHOT_ENABLED) ; } @@ -147,6 +149,12 @@ protected Control createPreferenceContent(@NotNull Composite parent) { UIConnectionMessages.pref_page_database_general_checkbox_show_row_count_tip, false, 1); + metadataSnapshotCheck = UIUtils.createCheckbox( + performanceGroup, + UIConnectionMessages.pref_page_database_general_checkbox_metadata_snapshot, + UIConnectionMessages.pref_page_database_general_checkbox_metadata_snapshot_tip, + false, + 1); } return composite; @@ -164,6 +172,7 @@ protected void loadPreferences(DBPPreferenceStore store) disableExtraMetadataRead.setSelection(store.getBoolean(ModelPreferences.META_DISABLE_EXTRA_READ)); addExtraDDLInfo.setSelection(store.getBoolean(ModelPreferences.META_EXTRA_DDL_INFO)); serverSideFiltersCheck.setSelection(store.getBoolean(ModelPreferences.META_USE_SERVER_SIDE_FILTERS)); + metadataSnapshotCheck.setSelection(store.getBoolean(ModelPreferences.META_SNAPSHOT_ENABLED)); } catch (Exception e) { log.warn(e); @@ -185,6 +194,7 @@ protected void savePreferences(DBPPreferenceStore store) store.setValue(ModelPreferences.META_DISABLE_EXTRA_READ, disableExtraMetadataRead.getSelection()); store.setValue(ModelPreferences.META_EXTRA_DDL_INFO, addExtraDDLInfo.getSelection()); store.setValue(ModelPreferences.META_USE_SERVER_SIDE_FILTERS, serverSideFiltersCheck.getSelection()); + store.setValue(ModelPreferences.META_SNAPSHOT_ENABLED, metadataSnapshotCheck.getSelection()); } catch (Exception e) { log.warn(e); @@ -201,6 +211,7 @@ protected void clearPreferences(DBPPreferenceStore store) store.setToDefault(ModelPreferences.META_DISABLE_EXTRA_READ); store.setToDefault(ModelPreferences.META_USE_SERVER_SIDE_FILTERS); store.setToDefault(ModelPreferences.META_EXTRA_DDL_INFO); + store.setToDefault(ModelPreferences.META_SNAPSHOT_ENABLED); } @Override @@ -214,6 +225,7 @@ protected void performDefaults() { disableExtraMetadataRead.setSelection(store.getDefaultBoolean(ModelPreferences.META_DISABLE_EXTRA_READ)); addExtraDDLInfo.setSelection(store.getDefaultBoolean(ModelPreferences.META_EXTRA_DDL_INFO)); serverSideFiltersCheck.setSelection(store.getDefaultBoolean(ModelPreferences.META_USE_SERVER_SIDE_FILTERS)); + metadataSnapshotCheck.setSelection(store.getDefaultBoolean(ModelPreferences.META_SNAPSHOT_ENABLED)); super.performDefaults(); } From d94e7ccb74eaf83cb7f0b7894a591019a7e9b5f2 Mon Sep 17 00:00:00 2001 From: Isabella Tessarotto Date: Sun, 18 Oct 2026 11:37:56 -0300 Subject: [PATCH 4/7] dbeaver/dbeaver#42473 Support read-once rows and change detection in metadata snapshot Assisted by: Claude Code --- .../impl/jdbc/cache/JDBCMetadataSnapshot.java | 79 ++++++++++++++++++- .../impl/jdbc/cache/JDBCObjectCache.java | 79 +++++++++++++++++-- .../impl/jdbc/cache/JDBCStructCache.java | 33 ++++++-- .../jdbc/cache/JDBCMetadataSnapshotTest.java | 29 +++++++ 4 files changed, 205 insertions(+), 15 deletions(-) diff --git a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshot.java b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshot.java index cb44712df9c7e..b10b774693802 100644 --- a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshot.java +++ b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshot.java @@ -59,6 +59,7 @@ import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -192,8 +193,23 @@ private Pass(@NotNull String setId, boolean fullList) { * only the pass is not committed. */ public void record(@NotNull String group, @NotNull ResultSet resultSet) { + Object[] row = capture(resultSet); + if (row != null) { + add(group, row); + } + } + + /** + * Copies the current row of the result set, for caches whose rows can be read only once + * (e.g. Oracle LONG columns): the object is then built from the copy with {@link #fetchCopy} + * and the copy is stored with {@link #add}. Errors are not thrown: the pass is not committed. + * + * @return the copy, or null if the row cannot be copied + */ + @Nullable + public Object[] capture(@NotNull ResultSet resultSet) { if (failed) { - return; + return null; } try { if (!Objects.equals(resultSet, lastResultSet)) { @@ -204,10 +220,35 @@ public void record(@NotNull String group, @NotNull ResultSet resultSet) { layout = newLayout; lastResultSet = resultSet; } - groups.computeIfAbsent(group, g -> new ArrayList<>()).add(layout.capture(resultSet)); + return layout.capture(resultSet); } catch (Exception e) { log.debug("Metadata row of " + setId + " not stored in snapshot: " + e.getMessage()); failed = true; + return null; + } + } + + /** + * Stores a row copied by {@link #capture} in a group. + */ + public void add(@NotNull String group, @NotNull Object[] row) { + if (!failed) { + groups.computeIfAbsent(group, g -> new ArrayList<>()).add(row); + } + } + + /** + * Builds an object from a row copied by {@link #capture}. + */ + @Nullable + public T fetchCopy( + @NotNull JDBCSession session, + @NotNull Object[] row, + @NotNull RowFetcher fetcher + ) throws SQLException, DBException { + RowReplay replay = new RowReplay(layout, Collections.singletonList(row)); + try (JDBCResultSet resultSet = JDBCResultSetImpl.makeResultSet(session, null, replay.newResultSet(), true)) { + return resultSet.next() ? fetcher.fetch(resultSet) : null; } } @@ -368,6 +409,40 @@ public synchronized Map getReadTimes(@NotNull String setId) { return result; } + /** + * Values of some columns in the stored rows, by group name: one string per row, with the values joined by "|" + * (date and time columns as epoch milliseconds). Used to compare stored rows with the database + * (e.g. DDL time and status of stored objects). + */ + @NotNull + public synchronized Map> getStoredValues(@NotNull String setId, @NotNull String... labels) { + readFile(); + RowSet set = sets.get(setId); + Map> result = new HashMap<>(); + if (set == null || set.layout == null) { + return result; + } + int[] indexes = new int[labels.length]; + for (int i = 0; i < labels.length; i++) { + indexes[i] = set.layout.indexOf(labels[i]); + } + for (Map.Entry e : set.groups.entrySet()) { + List values = new ArrayList<>(e.getValue().rows().size()); + for (Object[] row : e.getValue().rows()) { + StringBuilder value = new StringBuilder(); + for (int i = 0; i < indexes.length; i++) { + if (i > 0) { + value.append('|'); + } + value.append(indexes[i] < 0 ? null : row[indexes[i]]); + } + values.add(value.toString()); + } + result.put(e.getKey(), values); + } + return result; + } + public synchronized void removeGroup(@NotNull String setId, @NotNull String group) { readFile(); RowSet set = sets.get(setId); diff --git a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCObjectCache.java b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCObjectCache.java index 807346be74620..514fcb74c2228 100644 --- a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCObjectCache.java +++ b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCObjectCache.java @@ -56,7 +56,10 @@ public abstract class JDBCObjectCache fetchObject(session, owner, rs)) : + fetchObject(session, owner, dbResult); if (object == null || !isValidObject(monitor, owner, object)) { continue; } objects.add(object); - if (pass != null) { + if (row != null) { + pass.add(object.getName(), row); + } else if (pass != null && !isMetadataRowReadOnce()) { pass.record(object.getName(), dbResult); } @@ -245,6 +254,29 @@ protected boolean supportsMetadataSnapshot() { return false; } + /** + * Whether a row of the cache queries can be read only once (e.g. Oracle LONG columns). Then rows stored + * in the snapshot are copied before the object is built, and the object is built from the copy. + */ + protected boolean isMetadataRowReadOnce() { + return false; + } + + /** + * Reads names of the objects which changed since they were stored in the snapshot, for drivers which can tell + * it cheaply (e.g. by DDL time). Includes new and dropped objects. + * + * @return changed objects, or null if the driver cannot tell: then all restored metadata is read again + */ + @Nullable + protected Set readChangedObjects( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @NotNull JDBCMetadataSnapshot snapshot + ) throws DBException { + return null; + } + /** * Driver settings which change the rows read by this cache (e.g. options used in its queries). * A snapshot written with other settings is not restored. @@ -336,19 +368,56 @@ protected boolean isSnapshotRefreshPending() { return objectsRefreshPending; } + /** + * Updates metadata restored from the snapshot which changed since the snapshot was taken. + */ + private void refreshRestoredMetadata( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @NotNull JDBCMetadataSnapshot snapshot + ) throws DBException { + Set changed = readChangedObjects(monitor, owner, snapshot); + if (changed != null) { + log.debug(changed.size() + " objects of " + getCacheName() + " changed since the metadata snapshot"); + } + refreshRestoredObjects(monitor, owner, snapshot, changed); + refreshRestoredChildren(monitor, owner, snapshot, changed); + } + + /** + * Updates children restored from the snapshot (for caches with children). + * + * @param changed objects changed since the snapshot was taken, or null if unknown + */ + protected void refreshRestoredChildren( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @NotNull JDBCMetadataSnapshot snapshot, + @Nullable Set changed + ) throws DBException { + // No children + } + /** * Reads objects from the database and updates restored objects which changed since the snapshot was taken. * The database is read without the cache lock, so the cache stays usable meanwhile. + * + * @param changedObjects objects changed since the snapshot was taken, or null if unknown */ - protected void refreshRestoredMetadata( + private void refreshRestoredObjects( @NotNull DBRProgressMonitor monitor, @NotNull OWNER owner, - @NotNull JDBCMetadataSnapshot snapshot + @NotNull JDBCMetadataSnapshot snapshot, + @Nullable Set changedObjects ) throws DBException { if (!objectsRefreshPending) { return; } objectsRefreshPending = false; + if (changedObjects != null && changedObjects.isEmpty()) { + // Nothing changed: restored objects are up to date + return; + } if (!isFullyCached()) { // Cleared meanwhile: the next load reads the database return; diff --git a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCStructCache.java b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCStructCache.java index 8920fb96abcfb..29c6443c740b3 100644 --- a/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCStructCache.java +++ b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCStructCache.java @@ -190,15 +190,27 @@ private Map> readChildren( continue; } } + // Recorded even if already cached: the snapshot keeps all rows of the read. + // Rows which can be read only once are copied, and the child is built from the copy. + Object[] row = null; if (pass != null) { - // Recorded even if already cached: the snapshot keeps all rows of the read - pass.record(object.getName(), dbResult); + if (isMetadataRowReadOnce()) { + row = pass.capture(dbResult); + if (row != null) { + pass.add(object.getName(), row); + } + } else { + pass.record(object.getName(), dbResult); + } } if (skipCached && isChildrenCached(object)) { // Already read continue; } - CHILD child = fetchChild(session, owner, object, dbResult); + final OBJECT parent = object; + CHILD child = row != null ? + pass.fetchCopy(session, row, rs -> fetchChild(session, owner, parent, rs)) : + fetchChild(session, owner, object, dbResult); if (child == null) { continue; } @@ -296,22 +308,27 @@ protected boolean isSnapshotRefreshPending() { } @Override - protected void refreshRestoredMetadata( + protected void refreshRestoredChildren( @NotNull DBRProgressMonitor monitor, @NotNull OWNER owner, - @NotNull JDBCMetadataSnapshot snapshot + @NotNull JDBCMetadataSnapshot snapshot, + @Nullable Set changed ) throws DBException { - super.refreshRestoredMetadata(monitor, owner, snapshot); Set restored; synchronized (this) { restored = new LinkedHashSet<>(restoredChildren); restoredChildren.clear(); } + if (changed != null) { + // Children of unchanged objects are up to date + restored.retainAll(changed); + } if (restored.isEmpty() || monitor.isCanceled()) { return; } - // Children of all objects are read in one query, if all of them are stored. Otherwise, object by object. - boolean allObjects = snapshot.isComplete(SNAPSHOT_CHILDREN); + // Children of all objects are read in one query, if all of them are stored and the changed objects + // are not known. Otherwise, object by object. + boolean allObjects = changed == null && snapshot.isComplete(SNAPSHOT_CHILDREN); JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(SNAPSHOT_CHILDREN, allObjects); Map> children = new HashMap<>(); if (allObjects) { diff --git a/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java b/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java index 363aa2e287349..0a6f368954767 100644 --- a/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java +++ b/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java @@ -38,6 +38,7 @@ import java.sql.SQLFeatureNotSupportedException; import java.sql.Timestamp; import java.sql.Types; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -221,6 +222,34 @@ public void restoresAllGroups() throws Exception { Assertions.assertNull(snapshot.restoreGroups(session, TABLES, (group, rs) -> group)); } + @Test + public void buildsObjectsFromCopiedRows() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(TABLES, true); + ResultSet resultSet = tableResultSet(new Object[][]{{"T1", 1, CREATED}, {"T2", null, null}}); + List fetched = new ArrayList<>(); + while (resultSet.next()) { + Object[] row = pass.capture(resultSet); + Assertions.assertNotNull(row); + String name = pass.fetchCopy(session, row, rs -> rs.getString("TABLE_NAME") + ":" + rs.getTimestamp("CREATED")); + fetched.add(name); + pass.add(name.substring(0, 2), row); + } + pass.commit(); + Assertions.assertEquals(List.of("T1:" + CREATED, "T2:null"), fetched); + Assertions.assertEquals(List.of("T1", "T2"), restoreNames(snapshot, null)); + } + + @Test + public void returnsStoredValues() throws Exception { + JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); + recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}, {"T2", null, null}}); + Assertions.assertEquals( + Map.of("T1", List.of("1|" + CREATED.getTime()), "T2", List.of("null|null")), + snapshot.getStoredValues(TABLES, "ROW_COUNT", "CREATED")); + Assertions.assertEquals(Map.of(), snapshot.getStoredValues(COLUMNS, "ROW_COUNT")); + } + @Test public void clearForgetsSet() throws Exception { JDBCMetadataSnapshot snapshot = new JDBCMetadataSnapshot(file, SIGNATURE); From 9b72698d0bcf53d0dfc12ecf94f5f25df6071516 Mon Sep 17 00:00:00 2001 From: Isabella Tessarotto Date: Sun, 18 Oct 2026 11:37:57 -0300 Subject: [PATCH 5/7] dbeaver/dbeaver#42473 Keep Oracle table metadata between sessions Assisted by: Claude Code --- .../ext/oracle/model/OracleSchema.java | 94 ++++++++++++++++++- .../model/OracleSnapshotChangesTest.java | 56 +++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 test/org.jkiss.dbeaver.ext.oracle.test/src/org/jkiss/dbeaver/ext/oracle/model/OracleSnapshotChangesTest.java diff --git a/plugins/org.jkiss.dbeaver.ext.oracle/src/org/jkiss/dbeaver/ext/oracle/model/OracleSchema.java b/plugins/org.jkiss.dbeaver.ext.oracle/src/org/jkiss/dbeaver/ext/oracle/model/OracleSchema.java index 3f6e4bd7d28ad..45cef1744eb56 100644 --- a/plugins/org.jkiss.dbeaver.ext.oracle/src/org/jkiss/dbeaver/ext/oracle/model/OracleSchema.java +++ b/plugins/org.jkiss.dbeaver.ext.oracle/src/org/jkiss/dbeaver/ext/oracle/model/OracleSchema.java @@ -18,6 +18,7 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBDatabaseException; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.*; @@ -29,6 +30,7 @@ import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement; import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils; import org.jkiss.dbeaver.model.impl.jdbc.cache.JDBCCompositeCache; +import org.jkiss.dbeaver.model.impl.jdbc.cache.JDBCMetadataSnapshot; import org.jkiss.dbeaver.model.impl.jdbc.cache.JDBCObjectCache; import org.jkiss.dbeaver.model.impl.jdbc.cache.JDBCObjectLookupCache; import org.jkiss.dbeaver.model.impl.jdbc.cache.JDBCStructLookupCache; @@ -47,6 +49,7 @@ import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; import java.util.*; import java.util.stream.Collectors; @@ -734,6 +737,26 @@ public String getObjectDefinitionText(@NotNull DBRProgressMonitor monitor, @NotN + /** + * Objects whose dictionary rows (type, DDL time, status) differ from the stored ones, including new and dropped objects. + */ + @NotNull + static Set findChangedObjects(@NotNull Map> stored, @NotNull Map> current) { + Set changed = new HashSet<>(); + for (Map.Entry> entry : current.entrySet()) { + List storedValues = stored.get(entry.getKey()); + if (storedValues == null || !new HashSet<>(storedValues).equals(new HashSet<>(entry.getValue()))) { + changed.add(entry.getKey()); + } + } + for (String name : stored.keySet()) { + if (!current.containsKey(name)) { + changed.add(name); + } + } + return changed; + } + public class TableCache extends JDBCStructLookupCache { protected TableCache() @@ -742,9 +765,78 @@ protected TableCache() setListOrderComparator(DBUtils.nameComparator()); } + @Override + protected boolean supportsMetadataSnapshot() { + return true; + } + + @Override + protected boolean isMetadataRowReadOnce() { + // Column default values (DATA_DEFAULT) are LONG: they can be read only once + return true; + } + @NotNull @Override - public JDBCStatement prepareLookupStatement(@NotNull JDBCSession session, @NotNull OracleSchema owner, @Nullable OracleTableBase object, @Nullable String objectName) throws SQLException { + protected String[] getMetadataSnapshotSettings(@NotNull OracleSchema owner) { + DBPConnectionConfiguration cfg = getDataSource().getContainer().getConnectionConfiguration(); + return new String[]{ + CommonUtils.notEmpty(cfg.getProviderProperty(OracleConstants.PROP_ALWAYS_USE_DBA_VIEWS)), + CommonUtils.notEmpty(cfg.getProviderProperty(OracleConstants.PROP_METADATA_USE_ALTERNATIVE_TABLE_QUERY)), + CommonUtils.notEmpty(cfg.getProviderProperty(OracleConstants.PROP_METADATA_READ_COLUMN_COMMENTS)), + String.valueOf(getDataSource().isUseRuleHint()) + }; + } + + /** + * Compares type, DDL time and status of tables and views in the dictionary with the stored rows. + * DDL time changes on DDL of the object itself (columns, constraints, comments); the status changes + * when a view becomes invalid because of a change of the objects it depends on. + */ + @Nullable + @Override + protected Set readChangedObjects( + @NotNull DBRProgressMonitor monitor, + @NotNull OracleSchema owner, + @NotNull JDBCMetadataSnapshot snapshot + ) throws DBException { + if (CommonUtils.toBoolean(getDataSource().getContainer().getConnectionConfiguration() + .getProviderProperty(OracleConstants.PROP_METADATA_USE_ALTERNATIVE_TABLE_QUERY))) { + // The alternative query does not read DDL times + return null; + } + Map> stored = snapshot.getStoredValues( + SNAPSHOT_OBJECTS, OracleConstants.COLUMN_OBJECT_TYPE, OracleConstants.COLUMN_LAST_DDL_TIME, OracleConstants.COLUMN_STATUS); + Map> current = new HashMap<>(); + try (JDBCSession session = DBUtils.openMetaSession(monitor, owner, "Read changed tables of " + owner.getName())) { + try (JDBCPreparedStatement dbStat = session.prepareStatement( + "SELECT " + OracleUtils.getSysCatalogHint(getDataSource()) + " OBJECT_NAME, OBJECT_TYPE, LAST_DDL_TIME, STATUS FROM " + + OracleUtils.getAdminAllViewPrefix(monitor, getDataSource(), "OBJECTS") + + " WHERE OWNER=? AND OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')")) { + dbStat.setString(1, owner.getName()); + dbStat.setFetchSize(DBConstants.METADATA_FETCH_SIZE); + try (JDBCResultSet dbResult = dbStat.executeQuery()) { + while (dbResult.next()) { + Timestamp ddlTime = dbResult.getTimestamp(3); + current.computeIfAbsent(dbResult.getString(1), name -> new ArrayList<>()).add( + dbResult.getString(2) + "|" + (ddlTime == null ? null : ddlTime.getTime()) + "|" + dbResult.getString(4)); + } + } + } + } catch (SQLException e) { + throw new DBDatabaseException(e, getDataSource()); + } + return findChangedObjects(stored, current); + } + + @NotNull + @Override + public JDBCStatement prepareLookupStatement( + @NotNull JDBCSession session, + @NotNull OracleSchema owner, + @Nullable OracleTableBase object, + @Nullable String objectName + ) throws SQLException { String tableOper = "="; boolean hasAllAllTables = owner.getDataSource().isViewAvailable(session.getProgressMonitor(), null, "ALL_ALL_TABLES"); diff --git a/test/org.jkiss.dbeaver.ext.oracle.test/src/org/jkiss/dbeaver/ext/oracle/model/OracleSnapshotChangesTest.java b/test/org.jkiss.dbeaver.ext.oracle.test/src/org/jkiss/dbeaver/ext/oracle/model/OracleSnapshotChangesTest.java new file mode 100644 index 0000000000000..887a69b62eea1 --- /dev/null +++ b/test/org.jkiss.dbeaver.ext.oracle.test/src/org/jkiss/dbeaver/ext/oracle/model/OracleSnapshotChangesTest.java @@ -0,0 +1,56 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed 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.jkiss.dbeaver.ext.oracle.model; + +import org.jkiss.junit.DBeaverUnitTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class OracleSnapshotChangesTest extends DBeaverUnitTest { + + @Test + public void nothingChanged() { + Map> rows = Map.of( + "T1", List.of("TABLE|1000|VALID"), + "MV1", List.of("TABLE|2000|VALID", "MATERIALIZED VIEW|2000|VALID")); + Assertions.assertEquals(Set.of(), OracleSchema.findChangedObjects(rows, rows)); + } + + @Test + public void findsChangedNewAndDroppedObjects() { + Map> stored = Map.of( + "T1", List.of("TABLE|1000|VALID"), + "T2", List.of("TABLE|1000|VALID"), + "V1", List.of("VIEW|1000|VALID"), + "MV1", List.of("TABLE|2000|VALID", "MATERIALIZED VIEW|2000|VALID")); + Map> current = Map.of( + // DDL on T1 + "T1", List.of("TABLE|5000|VALID"), + // V1 invalidated by a change of an object it depends on: same DDL time + "V1", List.of("VIEW|1000|INVALID"), + // Same rows in another order + "MV1", List.of("MATERIALIZED VIEW|2000|VALID", "TABLE|2000|VALID"), + // Created + "T3", List.of("TABLE|6000|VALID")); + // T2 dropped + Assertions.assertEquals(Set.of("T1", "V1", "T2", "T3"), OracleSchema.findChangedObjects(stored, current)); + } +} From 4f8f7ac3ce6b770adac8f7de0c84ccba2cd70dfd Mon Sep 17 00:00:00 2001 From: Isabella Tessarotto Date: Sun, 18 Oct 2026 11:48:07 -0300 Subject: [PATCH 6/7] dbeaver/dbeaver#42474 Check result set navigation in snapshot tests Assisted by: Claude Code --- .../jdbc/cache/JDBCMetadataSnapshotTest.java | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java b/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java index 0a6f368954767..d937c73c0ea77 100644 --- a/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java +++ b/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java @@ -29,6 +29,8 @@ import org.junit.jupiter.api.io.TempDir; import org.mockito.Mockito; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -126,9 +128,8 @@ public void skipsPassWhenRowCannotBeRead() throws Exception { recordTables(snapshot, true, new Object[][]{{"T1", 1, CREATED}}); JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(TABLES, true); - ResultSet resultSet = tableResultSet(new Object[][]{{"T2", 2, CREATED}}); - resultSet.next(); - Mockito.when(resultSet.getString(2)).thenThrow(new SQLException("Unsupported type")); + ResultSet resultSet = tableResultSet(new Object[][]{{"T2", 2, CREATED}}, 2); + Assertions.assertTrue(resultSet.next()); pass.record("T2", resultSet); pass.commit(); Assertions.assertEquals(List.of("T1"), restoreNames(snapshot, null)); @@ -287,11 +288,18 @@ private static void recordTables( pass.commit(); } + @NotNull + private static ResultSet tableResultSet(@NotNull Object[][] rows) throws SQLException { + return tableResultSet(rows, 0); + } + /** * Result set with columns TABLE_NAME (varchar), ROW_COUNT (integer) and CREATED (timestamp). + * + * @param failingColumn column (from 1) whose value cannot be read as a string, or 0 */ @NotNull - private static ResultSet tableResultSet(@NotNull Object[][] rows) throws SQLException { + private static ResultSet tableResultSet(@NotNull Object[][] rows, int failingColumn) throws SQLException { ResultSetMetaData meta = Mockito.mock(ResultSetMetaData.class); Mockito.when(meta.getColumnCount()).thenReturn(3); Mockito.when(meta.getColumnLabel(1)).thenReturn("TABLE_NAME"); @@ -301,16 +309,24 @@ private static ResultSet tableResultSet(@NotNull Object[][] rows) throws SQLExce Mockito.when(meta.getColumnType(2)).thenReturn(Types.INTEGER); Mockito.when(meta.getColumnType(3)).thenReturn(Types.TIMESTAMP); - ResultSet resultSet = Mockito.mock(ResultSet.class); int[] position = {-1}; - Mockito.when(resultSet.getMetaData()).thenReturn(meta); - Mockito.when(resultSet.next()).thenAnswer(inv -> ++position[0] < rows.length); - Mockito.when(resultSet.getString(Mockito.anyInt())).thenAnswer(inv -> { - Object value = rows[position[0]][(int) inv.getArgument(0) - 1]; - return value == null ? null : value.toString(); - }); - Mockito.when(resultSet.getTimestamp(Mockito.anyInt())).thenAnswer( - inv -> (Timestamp) rows[position[0]][(int) inv.getArgument(0) - 1]); - return resultSet; + InvocationHandler handler = (proxy, method, args) -> switch (method.getName()) { + case "next" -> ++position[0] < rows.length; + case "getMetaData" -> meta; + case "getString" -> { + int column = (int) args[0]; + if (column == failingColumn) { + throw new SQLException("Unsupported type"); + } + Object value = rows[position[0]][column - 1]; + yield value == null ? null : value.toString(); + } + case "getTimestamp" -> rows[position[0]][(int) args[0] - 1]; + case "equals" -> proxy == args[0]; + case "hashCode" -> System.identityHashCode(proxy); + default -> throw new UnsupportedOperationException(method.getName()); + }; + return (ResultSet) Proxy.newProxyInstance( + JDBCMetadataSnapshotTest.class.getClassLoader(), new Class[]{ResultSet.class}, handler); } } From 9d842bfca09ca1a1ba5cdcdd16220a0a6421471a Mon Sep 17 00:00:00 2001 From: Isabella Tessarotto Date: Sun, 18 Oct 2026 12:08:13 -0300 Subject: [PATCH 7/7] dbeaver/dbeaver#42474 Iterate the result set in the failed row test Assisted by: Claude Code --- .../model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java b/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java index d937c73c0ea77..53f4eb0238b85 100644 --- a/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java +++ b/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java @@ -129,8 +129,9 @@ public void skipsPassWhenRowCannotBeRead() throws Exception { JDBCMetadataSnapshot.Pass pass = snapshot.beginPass(TABLES, true); ResultSet resultSet = tableResultSet(new Object[][]{{"T2", 2, CREATED}}, 2); - Assertions.assertTrue(resultSet.next()); - pass.record("T2", resultSet); + while (resultSet.next()) { + pass.record("T2", resultSet); + } pass.commit(); Assertions.assertEquals(List.of("T1"), restoreNames(snapshot, null)); }