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

mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new db8ea14d3f [Bug] Database perspective keeps old metadata around, fixes 
#8375 (#8376)
db8ea14d3f is described below

commit db8ea14d3f0995c30b8123cbd8583babb48929fe
Author: Matt Casters <[email protected]>
AuthorDate: Tue Sep 15 18:24:24 2026 +0200

    [Bug] Database perspective keeps old metadata around, fixes #8375 (#8376)
    
    - Implicitly reload metadata when connecting to a database or executing 
queries in the SQL editor tab
    - If the connection no longer exists in the metadata provider, remove it 
from the tree and abort
    - Fetch the latest metadata from the metadata serializer and update the 
connection state and open tabs
    - Synchronize open DatabaseSqlEditorTab and DatabaseTableInfoTab with the 
fresh DatabaseMeta
    - Add DatabaseWorkbenchReloadMetaTest unit tests
---
 .../perspective/database/DatabaseSqlEditorTab.java |  18 +-
 .../perspective/database/DatabaseTableInfoTab.java |  20 +-
 .../perspective/database/DatabaseWorkbench.java    | 259 +++++++++++++++++----
 .../database/messages/messages_en_US.properties    |   1 +
 .../database/DatabaseWorkbenchReloadMetaTest.java  | 185 +++++++++++++++
 5 files changed, 432 insertions(+), 51 deletions(-)

diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
index 34168870fa..d21c223586 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
@@ -28,6 +28,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import lombok.Getter;
+import lombok.Setter;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.hop.core.Const;
@@ -86,7 +87,7 @@ public class DatabaseSqlEditorTab implements 
IHopFileTypeHandler {
 
   private final IDatabaseWorkbenchHost host;
   private final DatabaseWorkbench workbench;
-  @Getter private final DatabaseMeta databaseMeta;
+  @Getter @Setter private DatabaseMeta databaseMeta;
   @Getter private final Composite control;
   @Getter private CTabItem tabItem;
 
@@ -376,6 +377,21 @@ public class DatabaseSqlEditorTab implements 
IHopFileTypeHandler {
     if (range == null || range.isBlank()) {
       return;
     }
+    if (databaseMeta != null) {
+      DatabaseMeta freshMeta = 
workbench.reloadConnectionMeta(databaseMeta.getName());
+      if (freshMeta == null) {
+        new ErrorDialog(
+            host.getShell(),
+            BaseMessages.getString(PKG, "DatabasePerspective.Error.Title"),
+            BaseMessages.getString(
+                PKG, "DatabasePerspective.Error.ConnectionNotFound", 
databaseMeta.getName()),
+            new HopException(
+                BaseMessages.getString(
+                    PKG, "DatabasePerspective.Error.ConnectionNotFound", 
databaseMeta.getName())));
+        return;
+      }
+      this.databaseMeta = freshMeta;
+    }
     workbench.ensureConnectedForExecute(databaseMeta, () -> 
runSqlStatements(range, caretInEditor));
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
index 308a8c5443..90ffb43cdc 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
@@ -27,10 +27,12 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import lombok.Getter;
+import lombok.Setter;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.database.Database;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseObjectDdl;
+import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.util.Utils;
@@ -38,6 +40,7 @@ import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.FormDataBuilder;
 import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.widget.ColumnInfo;
 import org.apache.hop.ui.core.widget.TableView;
@@ -64,7 +67,7 @@ public class DatabaseTableInfoTab implements 
IHopFileTypeHandler {
 
   private final IDatabaseWorkbenchHost host;
   private final DatabaseWorkbench workbench;
-  @Getter private final DatabaseMeta databaseMeta;
+  @Getter @Setter private DatabaseMeta databaseMeta;
   @Getter private final String schemaName;
   @Getter private final String tableName;
   @Getter private final DatabaseTreeNode.Kind kind;
@@ -175,6 +178,21 @@ public class DatabaseTableInfoTab implements 
IHopFileTypeHandler {
   }
 
   public void loadDetails() {
+    if (databaseMeta != null) {
+      DatabaseMeta freshMeta = 
workbench.reloadConnectionMeta(databaseMeta.getName());
+      if (freshMeta == null) {
+        new ErrorDialog(
+            host.getShell(),
+            BaseMessages.getString(PKG, "DatabasePerspective.Error.Title"),
+            BaseMessages.getString(
+                PKG, "DatabasePerspective.Error.ConnectionNotFound", 
databaseMeta.getName()),
+            new HopException(
+                BaseMessages.getString(
+                    PKG, "DatabasePerspective.Error.ConnectionNotFound", 
databaseMeta.getName())));
+        return;
+      }
+      this.databaseMeta = freshMeta;
+    }
     String qualified =
         databaseMeta.getQuotedSchemaTableCombination(host.getVariables(), 
schemaName, tableName);
     String description =
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
index e61f2d681c..0e9f94a03d 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
@@ -23,6 +23,7 @@ import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.database.Catalog;
 import org.apache.hop.core.database.Database;
@@ -38,6 +39,7 @@ import org.apache.hop.core.search.SearchMatcher;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.IHopMetadataSerializer;
 import org.apache.hop.ui.core.ConstUi;
 import org.apache.hop.ui.core.FormDataBuilder;
 import org.apache.hop.ui.core.PropsUi;
@@ -228,30 +230,32 @@ public class DatabaseWorkbench extends Composite 
implements TabClosable {
     horizontalSash.setWeights(22, 78);
     SashFormMemory.persist(horizontalSash, "database-workbench-tree-width", 
22, 78);
 
-    host.getHopGui()
-        .getEventsHandler()
-        .addEventListener(
-            eventListenerId,
-            e -> host.asyncExec(this::reloadConnections),
-            HopGuiEvents.MetadataChanged.name(),
-            HopGuiEvents.MetadataCreated.name(),
-            HopGuiEvents.MetadataDeleted.name());
-    host.getHopGui()
-        .getEventsHandler()
-        .addEventListener(
-            eventListenerId + "-project",
-            e ->
-                host.asyncExec(
-                    () -> {
-                      closeSqlEditorTabs();
-                      operationsPanel.cancelAll();
-                      operationsPanel.clearAll();
-                      reloadConnections();
-                      if (DatabaseSqlTabMemory.isOwner(this)) {
-                        DatabaseSqlTabMemory.restore(this);
-                      }
-                    }),
-            HopGuiEvents.ProjectActivated.name());
+    if (host.getHopGui() != null && host.getHopGui().getEventsHandler() != 
null) {
+      host.getHopGui()
+          .getEventsHandler()
+          .addEventListener(
+              eventListenerId,
+              e -> host.asyncExec(this::reloadConnections),
+              HopGuiEvents.MetadataChanged.name(),
+              HopGuiEvents.MetadataCreated.name(),
+              HopGuiEvents.MetadataDeleted.name());
+      host.getHopGui()
+          .getEventsHandler()
+          .addEventListener(
+              eventListenerId + "-project",
+              e ->
+                  host.asyncExec(
+                      () -> {
+                        closeSqlEditorTabs();
+                        operationsPanel.cancelAll();
+                        operationsPanel.clearAll();
+                        reloadConnections();
+                        if (DatabaseSqlTabMemory.isOwner(this)) {
+                          DatabaseSqlTabMemory.restore(this);
+                        }
+                      }),
+              HopGuiEvents.ProjectActivated.name());
+    }
 
     addDisposeListener(
         e -> {
@@ -262,8 +266,10 @@ public class DatabaseWorkbench extends Composite 
implements TabClosable {
           if (wasOwner) {
             DatabaseSqlTabMemory.restoreIntoRemaining(this);
           }
-          
host.getHopGui().getEventsHandler().removeEventListeners(eventListenerId);
-          
host.getHopGui().getEventsHandler().removeEventListeners(eventListenerId + 
"-project");
+          if (host.getHopGui() != null && host.getHopGui().getEventsHandler() 
!= null) {
+            
host.getHopGui().getEventsHandler().removeEventListeners(eventListenerId);
+            
host.getHopGui().getEventsHandler().removeEventListeners(eventListenerId + 
"-project");
+          }
         });
 
     DatabaseSqlTabMemory.register(this);
@@ -297,6 +303,7 @@ public class DatabaseWorkbench extends Composite implements 
TabClosable {
           state.setDatabaseMeta(meta);
         }
         connections.put(meta.getName(), state);
+        updateTabsDatabaseMeta(meta);
       }
     } catch (Exception e) {
       new ErrorDialog(
@@ -308,6 +315,106 @@ public class DatabaseWorkbench extends Composite 
implements TabClosable {
     rebuildTree();
   }
 
+  public void updateTabsDatabaseMeta(DatabaseMeta freshMeta) {
+    updateTabsDatabaseMeta(freshMeta, items);
+  }
+
+  static void updateTabsDatabaseMeta(DatabaseMeta freshMeta, 
List<TabItemHandler> items) {
+    if (freshMeta == null || items == null) {
+      return;
+    }
+    for (TabItemHandler item : items) {
+      if (item.getTypeHandler() instanceof DatabaseSqlEditorTab sqlTab) {
+        if (sqlTab.getDatabaseMeta() != null
+            && Objects.equals(sqlTab.getDatabaseMeta().getName(), 
freshMeta.getName())) {
+          sqlTab.setDatabaseMeta(freshMeta);
+        }
+      } else if (item.getTypeHandler() instanceof DatabaseTableInfoTab 
tableTab) {
+        if (tableTab.getDatabaseMeta() != null
+            && Objects.equals(tableTab.getDatabaseMeta().getName(), 
freshMeta.getName())) {
+          tableTab.setDatabaseMeta(freshMeta);
+        }
+      }
+    }
+  }
+
+  private void safeRebuildTree() {
+    if (isDisposed()) {
+      return;
+    }
+    if (Display.getCurrent() != null) {
+      rebuildTree();
+    } else {
+      host.asyncExec(this::rebuildTree);
+    }
+  }
+
+  public DatabaseMeta reloadConnectionMeta(String connectionName) {
+    return reloadConnectionMeta(connectionName, host, connections, items, 
this::safeRebuildTree);
+  }
+
+  static DatabaseMeta reloadConnectionMeta(
+      String connectionName,
+      IDatabaseWorkbenchHost host,
+      Map<String, DatabaseConnectionState> connections,
+      List<TabItemHandler> items,
+      Runnable onTreeChanged) {
+    if (Utils.isEmpty(connectionName)) {
+      return null;
+    }
+    if (host == null || host.getMetadataProvider() == null) {
+      DatabaseConnectionState state = connections == null ? null : 
connections.get(connectionName);
+      return state == null ? null : state.getDatabaseMeta();
+    }
+    try {
+      IHopMetadataSerializer<DatabaseMeta> serializer =
+          host.getMetadataProvider().getSerializer(DatabaseMeta.class);
+      if (!serializer.exists(connectionName)) {
+        if (connections != null) {
+          connections.remove(connectionName);
+        }
+        if (onTreeChanged != null) {
+          onTreeChanged.run();
+        }
+        return null;
+      }
+      DatabaseMeta freshMeta = serializer.load(connectionName);
+      if (freshMeta == null) {
+        if (connections != null) {
+          connections.remove(connectionName);
+        }
+        if (onTreeChanged != null) {
+          onTreeChanged.run();
+        }
+        return null;
+      }
+      if (connections != null) {
+        DatabaseConnectionState state = connections.get(connectionName);
+        if (state == null) {
+          state = new DatabaseConnectionState(freshMeta);
+          connections.put(connectionName, state);
+          if (onTreeChanged != null) {
+            onTreeChanged.run();
+          }
+        } else {
+          state.setDatabaseMeta(freshMeta);
+        }
+      }
+      updateTabsDatabaseMeta(freshMeta, items);
+      return freshMeta;
+    } catch (Exception e) {
+      if (host != null && host.getShell() != null) {
+        new ErrorDialog(
+            host.getShell(),
+            BaseMessages.getString(PKG, "DatabasePerspective.Error.Title"),
+            BaseMessages.getString(PKG, 
"DatabasePerspective.Error.LoadConnections"),
+            e);
+      }
+      DatabaseConnectionState state = connections == null ? null : 
connections.get(connectionName);
+      return state == null ? null : state.getDatabaseMeta();
+    }
+  }
+
   public void clearSearchFilter() {
     if (searchText != null && !searchText.isDisposed()) {
       searchText.setText("");
@@ -767,24 +874,38 @@ public class DatabaseWorkbench extends Composite 
implements TabClosable {
    * thread after a successful connect.
    */
   public void connect(DatabaseConnectionState state, Runnable afterConnected) {
-    DatabaseMeta meta = state.getDatabaseMeta();
+    if (state == null || state.getDatabaseMeta() == null) {
+      return;
+    }
+    String connectionName = state.getDatabaseMeta().getName();
+    DatabaseMeta freshMeta = reloadConnectionMeta(connectionName);
+    if (freshMeta == null) {
+      return;
+    }
+    DatabaseConnectionState targetState = connections.get(freshMeta.getName());
+    if (targetState == null) {
+      targetState = state;
+    }
+    targetState.setDatabaseMeta(freshMeta);
+    final DatabaseConnectionState connectState = targetState;
     String description =
-        BaseMessages.getString(PKG, "DatabasePerspective.Operation.Connect", 
meta.getName());
+        BaseMessages.getString(PKG, "DatabasePerspective.Operation.Connect", 
freshMeta.getName());
     runOperation(
         description,
-        meta.getName(),
+        freshMeta.getName(),
         operation -> {
-          DatabaseMetaInformation info = new 
DatabaseMetaInformation(host.getVariables(), meta);
+          DatabaseMetaInformation info =
+              new DatabaseMetaInformation(host.getVariables(), freshMeta);
           info.getData(host.getLoggingObject(), operation.newMonitor());
           if (operation.isCancelled()) {
             return;
           }
           host.asyncExec(
               () -> {
-                state.setInformation(info);
-                state.setConnected(true);
+                connectState.setInformation(info);
+                connectState.setConnected(true);
                 rebuildTree();
-                selectConnection(meta.getName());
+                selectConnection(freshMeta.getName());
                 if (afterConnected != null) {
                   afterConnected.run();
                 }
@@ -803,6 +924,9 @@ public class DatabaseWorkbench extends Composite implements 
TabClosable {
       return false;
     }
     DatabaseConnectionState state = ensureConnection(meta);
+    if (state == null) {
+      return false;
+    }
     if (state.isConnected()) {
       afterConnected.run();
       return true;
@@ -902,17 +1026,19 @@ public class DatabaseWorkbench extends Composite 
implements TabClosable {
     if (state == null) {
       return;
     }
+    DatabaseMeta freshMeta = 
reloadConnectionMeta(state.getDatabaseMeta().getName());
+    if (freshMeta == null) {
+      return;
+    }
     DatabaseTreeNode node = selectedNode();
     String sql = "";
     if (node != null && node.isTableLike()) {
       String qualified =
-          state
-              .getDatabaseMeta()
-              .getQuotedSchemaTableCombination(
-                  host.getVariables(), node.getSchemaName(), 
node.getObjectName());
+          freshMeta.getQuotedSchemaTableCombination(
+              host.getVariables(), node.getSchemaName(), node.getObjectName());
       sql = "SELECT * FROM " + qualified;
     }
-    openSqlTab(state.getDatabaseMeta(), sql, null, sql, false);
+    openSqlTab(freshMeta, sql, null, sql, false);
   }
 
   @GuiToolbarElement(
@@ -932,7 +1058,11 @@ public class DatabaseWorkbench extends Composite 
implements TabClosable {
     if (node == null || state == null || !node.isTableLike()) {
       return;
     }
-    DatabaseMeta meta = state.getDatabaseMeta();
+    DatabaseMeta freshMeta = 
reloadConnectionMeta(state.getDatabaseMeta().getName());
+    if (freshMeta == null) {
+      return;
+    }
+    DatabaseMeta meta = freshMeta;
     String qualified =
         meta.getQuotedSchemaTableCombination(
             host.getVariables(), node.getSchemaName(), node.getObjectName());
@@ -971,7 +1101,11 @@ public class DatabaseWorkbench extends Composite 
implements TabClosable {
     if (node == null || state == null || !node.isTableLike()) {
       return;
     }
-    DatabaseMeta meta = state.getDatabaseMeta();
+    DatabaseMeta freshMeta = 
reloadConnectionMeta(state.getDatabaseMeta().getName());
+    if (freshMeta == null) {
+      return;
+    }
+    DatabaseMeta meta = freshMeta;
     String sql =
         previewSelectSql(
             meta,
@@ -1013,8 +1147,11 @@ public class DatabaseWorkbench extends Composite 
implements TabClosable {
     if (node == null || state == null || !node.isTableLike()) {
       return;
     }
-    openTableInfo(
-        state.getDatabaseMeta(), node.getSchemaName(), node.getObjectName(), 
node.getKind());
+    DatabaseMeta freshMeta = 
reloadConnectionMeta(state.getDatabaseMeta().getName());
+    if (freshMeta == null) {
+      return;
+    }
+    openTableInfo(freshMeta, node.getSchemaName(), node.getObjectName(), 
node.getKind());
   }
 
   @GuiToolbarElement(
@@ -1133,21 +1270,45 @@ public class DatabaseWorkbench extends Composite 
implements TabClosable {
       return;
     }
     DatabaseConnectionState state = ensureConnection(meta);
-    selectConnection(meta.getName());
-    openSqlTab(meta, Const.NVL(sql, ""), null, Const.NVL(sql, ""), true);
+    if (state == null) {
+      return;
+    }
+    DatabaseMeta effectiveMeta = state.getDatabaseMeta();
+    selectConnection(effectiveMeta.getName());
+    openSqlTab(effectiveMeta, Const.NVL(sql, ""), null, Const.NVL(sql, ""), 
true);
     if (!state.isConnected()) {
       connect(state);
     }
   }
 
   DatabaseConnectionState ensureConnection(DatabaseMeta meta) {
-    DatabaseConnectionState state = connections.get(meta.getName());
+    return ensureConnection(meta, host, connections, items, 
this::safeRebuildTree);
+  }
+
+  static DatabaseConnectionState ensureConnection(
+      DatabaseMeta meta,
+      IDatabaseWorkbenchHost host,
+      Map<String, DatabaseConnectionState> connections,
+      List<TabItemHandler> items,
+      Runnable onTreeChanged) {
+    if (meta == null || connections == null) {
+      return null;
+    }
+    DatabaseMeta freshMeta =
+        reloadConnectionMeta(meta.getName(), host, connections, items, 
onTreeChanged);
+    if (freshMeta == null && host != null && host.getMetadataProvider() != 
null) {
+      return null;
+    }
+    DatabaseMeta effectiveMeta = freshMeta != null ? freshMeta : meta;
+    DatabaseConnectionState state = connections.get(effectiveMeta.getName());
     if (state == null) {
-      state = new DatabaseConnectionState(meta);
-      connections.put(meta.getName(), state);
-      rebuildTree();
+      state = new DatabaseConnectionState(effectiveMeta);
+      connections.put(effectiveMeta.getName(), state);
+      if (onTreeChanged != null) {
+        onTreeChanged.run();
+      }
     } else {
-      state.setDatabaseMeta(meta);
+      state.setDatabaseMeta(effectiveMeta);
     }
     return state;
   }
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/database/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/database/messages/messages_en_US.properties
index bba654a925..cb8c4fbfab 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/database/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/database/messages/messages_en_US.properties
@@ -104,3 +104,4 @@ DatabasePerspective.OpenInDatabase.Message=Select the 
database connection to run
 DatabasePerspective.OpenInDatabase.Tooltip=Open this SQL file in the Database 
perspective
 DatabasePerspective.Dialog.Title=Database
 DatabasePerspective.Menu.Tools.Window=Database window
+DatabasePerspective.Error.ConnectionNotFound=Database connection ''{0}'' no 
longer exists.
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchReloadMetaTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchReloadMetaTest.java
new file mode 100644
index 0000000000..71bc2eb7c5
--- /dev/null
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchReloadMetaTest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.hop.ui.hopgui.perspective.database;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.api.IHopMetadataSerializer;
+import org.apache.hop.ui.hopgui.perspective.TabItemHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class DatabaseWorkbenchReloadMetaTest {
+
+  private IDatabaseWorkbenchHost host;
+  private IHopMetadataProvider metadataProvider;
+  private IHopMetadataSerializer<DatabaseMeta> serializer;
+
+  @BeforeEach
+  @SuppressWarnings("unchecked")
+  void setUp() throws Exception {
+    metadataProvider = mock(IHopMetadataProvider.class);
+    serializer = (IHopMetadataSerializer<DatabaseMeta>) 
mock(IHopMetadataSerializer.class);
+    
when(metadataProvider.getSerializer(DatabaseMeta.class)).thenReturn(serializer);
+
+    host = mock(IDatabaseWorkbenchHost.class);
+    when(host.getMetadataProvider()).thenReturn(metadataProvider);
+  }
+
+  @Test
+  void reloadConnectionMetaReturnsNullForEmpty() {
+    Map<String, DatabaseConnectionState> connections = new LinkedHashMap<>();
+    assertNull(DatabaseWorkbench.reloadConnectionMeta(null, host, connections, 
List.of(), null));
+    assertNull(DatabaseWorkbench.reloadConnectionMeta("", host, connections, 
List.of(), null));
+  }
+
+  @Test
+  void reloadConnectionMetaRemovesDeletedConnection() throws Exception {
+    Map<String, DatabaseConnectionState> connections = new LinkedHashMap<>();
+    DatabaseMeta meta = mock(DatabaseMeta.class);
+    when(meta.getName()).thenReturn("deleted-conn");
+    connections.put("deleted-conn", new DatabaseConnectionState(meta));
+
+    when(serializer.exists("deleted-conn")).thenReturn(false);
+
+    AtomicBoolean treeChanged = new AtomicBoolean(false);
+    DatabaseMeta result =
+        DatabaseWorkbench.reloadConnectionMeta(
+            "deleted-conn", host, connections, List.of(), () -> 
treeChanged.set(true));
+
+    assertNull(result);
+    assertFalse(connections.containsKey("deleted-conn"));
+    assertTrue(treeChanged.get());
+  }
+
+  @Test
+  void reloadConnectionMetaUpdatesExistingStateAndOpenTabs() throws Exception {
+    Map<String, DatabaseConnectionState> connections = new LinkedHashMap<>();
+    DatabaseMeta staleMeta = mock(DatabaseMeta.class);
+    when(staleMeta.getName()).thenReturn("my-conn");
+    when(staleMeta.getHostname()).thenReturn("old-host");
+    connections.put("my-conn", new DatabaseConnectionState(staleMeta));
+
+    DatabaseMeta freshMeta = mock(DatabaseMeta.class);
+    when(freshMeta.getName()).thenReturn("my-conn");
+    when(freshMeta.getHostname()).thenReturn("new-host");
+
+    when(serializer.exists("my-conn")).thenReturn(true);
+    when(serializer.load("my-conn")).thenReturn(freshMeta);
+
+    DatabaseSqlEditorTab sqlTab = mock(DatabaseSqlEditorTab.class);
+    when(sqlTab.getDatabaseMeta()).thenReturn(staleMeta);
+    TabItemHandler sqlHandler = mock(TabItemHandler.class);
+    when(sqlHandler.getTypeHandler()).thenReturn(sqlTab);
+
+    DatabaseTableInfoTab tableTab = mock(DatabaseTableInfoTab.class);
+    when(tableTab.getDatabaseMeta()).thenReturn(staleMeta);
+    TabItemHandler tableHandler = mock(TabItemHandler.class);
+    when(tableHandler.getTypeHandler()).thenReturn(tableTab);
+
+    DatabaseMeta result =
+        DatabaseWorkbench.reloadConnectionMeta(
+            "my-conn", host, connections, List.of(sqlHandler, tableHandler), 
null);
+
+    assertSame(freshMeta, result);
+    assertSame(freshMeta, connections.get("my-conn").getDatabaseMeta());
+    verify(sqlTab).setDatabaseMeta(freshMeta);
+    verify(tableTab).setDatabaseMeta(freshMeta);
+  }
+
+  @Test
+  void ensureConnectionReturnsNullWhenDeleted() throws Exception {
+    Map<String, DatabaseConnectionState> connections = new LinkedHashMap<>();
+    DatabaseMeta meta = mock(DatabaseMeta.class);
+    when(meta.getName()).thenReturn("deleted-conn");
+    connections.put("deleted-conn", new DatabaseConnectionState(meta));
+
+    when(serializer.exists("deleted-conn")).thenReturn(false);
+
+    DatabaseConnectionState state =
+        DatabaseWorkbench.ensureConnection(meta, host, connections, List.of(), 
null);
+
+    assertNull(state);
+    assertFalse(connections.containsKey("deleted-conn"));
+  }
+
+  @Test
+  void ensureConnectionReturnsFreshStateWhenExists() throws Exception {
+    Map<String, DatabaseConnectionState> connections = new LinkedHashMap<>();
+    DatabaseMeta staleMeta = mock(DatabaseMeta.class);
+    when(staleMeta.getName()).thenReturn("conn1");
+    when(staleMeta.getHostname()).thenReturn("old-host");
+    connections.put("conn1", new DatabaseConnectionState(staleMeta));
+
+    DatabaseMeta freshMeta = mock(DatabaseMeta.class);
+    when(freshMeta.getName()).thenReturn("conn1");
+    when(freshMeta.getHostname()).thenReturn("new-host");
+
+    when(serializer.exists("conn1")).thenReturn(true);
+    when(serializer.load("conn1")).thenReturn(freshMeta);
+
+    DatabaseConnectionState state =
+        DatabaseWorkbench.ensureConnection(staleMeta, host, connections, 
List.of(), null);
+
+    assertNotNull(state);
+    assertSame(freshMeta, state.getDatabaseMeta());
+    assertEquals("new-host", state.getDatabaseMeta().getHostname());
+  }
+
+  @Test
+  void updateTabsDatabaseMetaUpdatesMatchingTabsOnly() {
+    DatabaseMeta freshMeta = mock(DatabaseMeta.class);
+    when(freshMeta.getName()).thenReturn("match");
+
+    DatabaseMeta matchMeta = mock(DatabaseMeta.class);
+    when(matchMeta.getName()).thenReturn("match");
+
+    DatabaseMeta otherMeta = mock(DatabaseMeta.class);
+    when(otherMeta.getName()).thenReturn("other");
+
+    DatabaseSqlEditorTab matchTab = mock(DatabaseSqlEditorTab.class);
+    when(matchTab.getDatabaseMeta()).thenReturn(matchMeta);
+    TabItemHandler matchHandler = mock(TabItemHandler.class);
+    when(matchHandler.getTypeHandler()).thenReturn(matchTab);
+
+    DatabaseSqlEditorTab otherTab = mock(DatabaseSqlEditorTab.class);
+    when(otherTab.getDatabaseMeta()).thenReturn(otherMeta);
+    TabItemHandler otherHandler = mock(TabItemHandler.class);
+    when(otherHandler.getTypeHandler()).thenReturn(otherTab);
+
+    DatabaseWorkbench.updateTabsDatabaseMeta(freshMeta, List.of(matchHandler, 
otherHandler));
+
+    verify(matchTab).setDatabaseMeta(freshMeta);
+    verify(otherTab, never()).setDatabaseMeta(freshMeta);
+  }
+}

Reply via email to