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 0674224c544a..b7bea34c4f47 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 cb14cdeeb092..5454443da5b4 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 fc54b0f6c534..b8e547ebfcdc 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 e4346ac1b74d..6470857b9f19 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 e85ab490e4c8..158e3d533d4f 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 06faca10e50d..e7179b4b1fb0 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 37a41df6b5e7..25ffdbec99d1 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 2292ffed8b21..cfb2d505bed5 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 2c933fed7628..d234b3b87981 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 9889f9beba62..129263ca5bb0 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 c24cb973fbb5..bda884cd4091 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 79dde79db005..b8036f3cf5ba 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 c19280d63295..9632086c1bf2 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 e089d89575f8..ac28fa10695e 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 88645e6691bc..854e4499f98d 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 fc985f814eaa..be077c83e3f7 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.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 c153a7104c4b..fdcd9bffdd2d 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.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 d765ede50af1..0a3967c43e43 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 b039368fb73f..80b8c9e2561b 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 620530c75c47..243f86449561 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 048760283114..1211026fd17a 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 12e087b412e3..8cae0b2411bc 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 95053d062676..5d441ed39ef0 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 d47d73b39e79..e435d8ab241a 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 3a17e266b724..c9f6d07e728b 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.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 3f6e4bd7d28a..45cef1744eb5 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/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 940763b8651a..667c2d8e21df 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 1928ad43f5ce..8611d156746d 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 a3c4afd686a0..a1e9a43dee9c 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 1fae66e41055..1ccbe181244f 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 b8536c9fc4f4..c4b266bf4328 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 b61d9e68cf51..c93ab4d138e3 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 aced14cd9776..47cce4429856 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 321d1888fd44..5345beca0d58 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 6884a80ee2fa..c9b702cf9492 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.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 000000000000..b10b77469380 --- /dev/null +++ b/plugins/org.jkiss.dbeaver.model.jdbc/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshot.java @@ -0,0 +1,998 @@ +/* + * 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.Collections; +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) { + 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 null; + } + 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; + } + 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; + } + } + + /** + * 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; + } + + /** + * 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); + 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/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 6fb15e3683a3..514fcb74c222 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,23 @@ 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 +154,323 @@ protected synchronized void loadObjects(DBRProgressMonitor monitor, OWNER owner) } } } + finishLoading(monitor, owner, tmpObjectList); + } - addCustomObjects(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; + } + + // Rows which can be read only once are copied first, and the object is built from the copy + Object[] row = pass != null && isMetadataRowReadOnce() ? pass.capture(dbResult) : null; + OBJECT object = row != null ? + pass.fetchCopy(session, row, rs -> fetchObject(session, owner, rs)) : + fetchObject(session, owner, dbResult); + if (object == null || !isValidObject(monitor, owner, object)) { + continue; + } + objects.add(object); + if (row != null) { + pass.add(object.getName(), row); + } else if (pass != null && !isMetadataRowReadOnce()) { + 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; + } + + /** + * 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. + */ + @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; + } + + /** + * 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 + */ + private void refreshRestoredObjects( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @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; + } + 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 +488,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 41b34438c735..29c6443c740b 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,11 +190,27 @@ public synchronized void loadChildren(DBRProgressMonitor monitor, OWNER owner, @ continue; } } - if (isChildrenCached(object)) { + // 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) { + 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; } @@ -136,32 +221,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 +231,179 @@ 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 refreshRestoredChildren( + @NotNull DBRProgressMonitor monitor, + @NotNull OWNER owner, + @NotNull JDBCMetadataSnapshot snapshot, + @Nullable Set changed + ) throws DBException { + 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 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) { + 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 +461,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.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 86c774ef2b17..c48c6b94c5a0 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 cf1efdb77d5a..7f02d2696a62 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/ModelPreferences.java b/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/ModelPreferences.java index 6df651ed28d2..0efb4db969f6 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.model/src/org/jkiss/dbeaver/model/DBUtils.java b/plugins/org.jkiss.dbeaver.model/src/org/jkiss/dbeaver/model/DBUtils.java index 0e3388d736fd..f47c81936ea5 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 a92685892f9c..6b1d3db98aca 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 c8930ec7d952..c5a5c2f1a3f8 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 683580eee636..93b140b499b8 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 abcfabd777d3..5969c745266e 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.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 dbb93b932973..6638b438d301 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 f8bc326a3b12..d835731b88e6 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 0c202b388352..0fc8c9ff70e6 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 e02d0f2952f3..4e50dc701881 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(); } 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 113dc5f5516f..2334c4c11029 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 b45971219557..066b0664091b 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 f8b3e0f9ed7f..c10a6d1f0778 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 a4464a660633..1c935f0c5b19 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 77fd8956aa67..eb70e83e6b60 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 da710118140d..37903400253b 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 c860d48176a7..87cea25c1900 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/OracleSnapshotChangesTest.java b/test/org.jkiss.dbeaver.ext.oracle.test/src/org/jkiss/dbeaver/ext/oracle/model/OracleSnapshotChangesTest.java new file mode 100644 index 000000000000..887a69b62eea --- /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)); + } +} 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 6f17ff45ca18..a401c7282fc1 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); 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 000000000000..b23537a15986 --- /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 000000000000..14fb1734d9f7 --- /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 000000000000..c5823fc6c107 --- /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 000000000000..53f4eb0238b8 --- /dev/null +++ b/test/org.jkiss.dbeaver.model.jdbc.test/src/org/jkiss/dbeaver/model/impl/jdbc/cache/JDBCMetadataSnapshotTest.java @@ -0,0 +1,333 @@ +/* + * 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.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +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.ArrayList; +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}}, 2); + while (resultSet.next()) { + 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 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); + 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(); + } + + @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, int failingColumn) 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); + + int[] position = {-1}; + 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); + } +} diff --git a/test/pom.xml b/test/pom.xml index 2725533228a7..161ea4f11241 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