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

hansva 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 44d18ff53c fix eager metadata loading, fixes #8203 (#8210)
44d18ff53c is described below

commit 44d18ff53cfbb77f2b74e84dff954c4ef38d5cce
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Sep 1 16:46:51 2026 +0200

    fix eager metadata loading, fixes #8203 (#8210)
---
 .../xp/LocationMouseDoubleClickExtensionPoint.java | 198 ++++++++++-----------
 .../testing/PipelineUnitTestSetLocationDialog.java |  39 +++-
 .../PipelineUnitTestSetLocationDialogTest.java     | 175 ++++++++++++++++++
 .../transforms/tableinput/TableInputMeta.java      |   9 +
 .../tableinput/messages/messages_en_US.properties  |   1 +
 .../transforms/tableinput/TableInputMetaTest.java  |  24 +++
 6 files changed, 332 insertions(+), 114 deletions(-)

diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/LocationMouseDoubleClickExtensionPoint.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/LocationMouseDoubleClickExtensionPoint.java
index b83a657208..da459d849f 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/LocationMouseDoubleClickExtensionPoint.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/LocationMouseDoubleClickExtensionPoint.java
@@ -17,9 +17,8 @@
 
 package org.apache.hop.testing.xp;
 
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
+import java.util.function.Function;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.extension.ExtensionPoint;
 import org.apache.hop.core.extension.IExtensionPoint;
@@ -31,7 +30,6 @@ import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.engine.IPipelineEngine;
-import org.apache.hop.pipeline.transform.TransformMeta;
 import org.apache.hop.testing.DataSet;
 import org.apache.hop.testing.PipelineUnitTest;
 import org.apache.hop.testing.PipelineUnitTestSetLocation;
@@ -39,7 +37,6 @@ import org.apache.hop.testing.UnitTestResult;
 import org.apache.hop.testing.gui.TestingGuiPlugin;
 import org.apache.hop.testing.util.DataSetConst;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
-import org.apache.hop.ui.core.metadata.MetadataManager;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
 import 
org.apache.hop.ui.hopgui.file.pipeline.extension.HopGuiPipelineGraphExtension;
@@ -65,119 +62,104 @@ public class LocationMouseDoubleClickExtensionPoint
       return;
     }
 
+    // This is called for every mouse down on the canvas so figure out first 
whether one of the
+    // unit test markers was clicked on.  Only then do any real work: loading 
the data sets and
+    // determining transform fields is far too expensive to do on every click 
(issue #8203).
+    //
+    MouseEvent e = pipelineGraphExtension.getEvent();
+    if (e.button != 1 && e.button != 2) {
+      return;
+    }
+    Point point = pipelineGraphExtension.getPoint();
+    AreaOwner areaOwner = pipelineGraph.getVisibleAreaOwner(point.x, point.y);
+    if (areaOwner == null || areaOwner.getAreaType() == null) {
+      return;
+    }
+    Object area = areaOwner.getParent();
+    boolean inputDataSet = DataSetConst.AREA_DRAWN_INPUT_DATA_SET.equals(area);
+    boolean goldenDataSet = 
DataSetConst.AREA_DRAWN_GOLDEN_DATA_SET.equals(area);
+    boolean goldenDataResult = 
DataSetConst.AREA_DRAWN_GOLDEN_DATA_RESULT.equals(area);
+    if (!inputDataSet && !goldenDataSet && !goldenDataResult) {
+      return;
+    }
+
     HopGui hopGui = HopGui.getInstance();
     try {
-      List<DataSet> dataSets = 
hopGui.getMetadataProvider().getSerializer(DataSet.class).loadAll();
+      String transformName = (String) areaOwner.getOwner();
+
+      if (goldenDataResult) {
+        pipelineGraphExtension.setPreventingDefault(true);
+        showGoldenDataResult(pipelineGraph, hopGui, unitTest, transformName);
+        return;
+      }
 
-      Map<String, IRowMeta> transformFieldsMap = new HashMap<>();
-      for (TransformMeta transformMeta : pipelineMeta.getTransforms()) {
-        try {
-          IRowMeta transformFields =
-              pipelineMeta.getTransformFields(pipelineGraph.getVariables(), 
transformMeta);
-          transformFieldsMap.put(transformMeta.getName(), transformFields);
-        } catch (Exception e) {
-          // Ignore GUI errors...
-        }
+      pipelineGraphExtension.setPreventingDefault(true);
+
+      PipelineUnitTestSetLocation location =
+          inputDataSet
+              ? unitTest.findInputLocation(transformName)
+              : unitTest.findGoldenLocation(transformName);
+      if (location == null) {
+        return;
       }
 
-      // Find the location that was double-clicked on...
-      //
-      MouseEvent e = pipelineGraphExtension.getEvent();
-      Point point = pipelineGraphExtension.getPoint();
-
-      if (e.button == 1 || e.button == 2) {
-        AreaOwner areaOwner = pipelineGraph.getVisibleAreaOwner(point.x, 
point.y);
-        if (areaOwner != null && areaOwner.getAreaType() != null) {
-          // Check if this is the flask...
-          //
-          if 
(DataSetConst.AREA_DRAWN_INPUT_DATA_SET.equals(areaOwner.getParent())) {
-            pipelineGraphExtension.setPreventingDefault(true);
-
-            // Open the dataset double-clicked on...
-            //
-            String transformName = (String) areaOwner.getOwner();
-
-            PipelineUnitTestSetLocation inputLocation = 
unitTest.findInputLocation(transformName);
-            if (inputLocation != null) {
-              pipelineGraphExtension.setPreventingDefault(true);
-              PipelineUnitTestSetLocationDialog dialog =
-                  new PipelineUnitTestSetLocationDialog(
-                      hopGui.getActiveShell(),
-                      variables,
-                      hopGui.getMetadataProvider(),
-                      inputLocation,
-                      dataSets,
-                      transformFieldsMap);
-              if (dialog.open()) {
-                
hopGui.getMetadataProvider().getSerializer(PipelineUnitTest.class).save(unitTest);
-                pipelineGraph.updateGui();
-              }
-            }
-          } else if 
(DataSetConst.AREA_DRAWN_GOLDEN_DATA_SET.equals(areaOwner.getParent())) {
-            pipelineGraphExtension.setPreventingDefault(true);
-
-            // Open the dataset double-clicked on...
-            //
-            String transformName = (String) areaOwner.getOwner();
-
-            PipelineUnitTestSetLocation goldenLocation = 
unitTest.findGoldenLocation(transformName);
-            if (goldenLocation != null) {
-              pipelineGraphExtension.setPreventingDefault(true);
-              PipelineUnitTestSetLocationDialog dialog =
-                  new PipelineUnitTestSetLocationDialog(
-                      hopGui.getActiveShell(),
-                      variables,
-                      hopGui.getMetadataProvider(),
-                      goldenLocation,
-                      dataSets,
-                      transformFieldsMap);
-              if (dialog.open()) {
-                // Save the unit test
-                
hopGui.getMetadataProvider().getSerializer(PipelineUnitTest.class).save(unitTest);
-                pipelineGraph.updateGui();
-              }
-            }
-          } else if 
(DataSetConst.AREA_DRAWN_GOLDEN_DATA_RESULT.equals(areaOwner.getParent())) {
-            pipelineGraphExtension.setPreventingDefault(true);
-
-            // Open the dataset double-clicked on...
-            //
-            String transformName = (String) areaOwner.getOwner();
-
-            PipelineUnitTestSetLocation goldenLocation = 
unitTest.findGoldenLocation(transformName);
-            if (goldenLocation != null) {
-
-              // Find the errors list of the unit test...
-              //
-              IPipelineEngine<PipelineMeta> pipeline = 
pipelineGraph.getPipeline();
-              if (pipeline == null) {
-                return;
-              }
-
-              List<UnitTestResult> results =
-                  (List<UnitTestResult>)
-                      
pipeline.getExtensionDataMap().get(DataSetConst.UNIT_TEST_RESULTS);
-              if (Utils.isEmpty(results)) {
-                return;
-              }
-
-              pipelineGraphExtension.setPreventingDefault(true);
-              
ValidatePipelineUnitTestExtensionPoint.showUnitTestErrors(pipeline, results, 
hopGui);
-            }
-          }
-        }
+      List<DataSet> dataSets = 
hopGui.getMetadataProvider().getSerializer(DataSet.class).loadAll();
+
+      PipelineUnitTestSetLocationDialog dialog =
+          new PipelineUnitTestSetLocationDialog(
+              hopGui.getActiveShell(),
+              variables,
+              hopGui.getMetadataProvider(),
+              location,
+              dataSets,
+              pipelineMeta.getTransformNames(),
+              transformFieldsResolver(pipelineGraph, pipelineMeta));
+      if (dialog.open()) {
+        
hopGui.getMetadataProvider().getSerializer(PipelineUnitTest.class).save(unitTest);
+        pipelineGraph.updateGui();
       }
-    } catch (Exception e) {
-      new ErrorDialog(hopGui.getActiveShell(), "Error", "Error editing 
location", e);
+    } catch (Exception e2) {
+      new ErrorDialog(hopGui.getActiveShell(), "Error", "Error editing 
location", e2);
     }
   }
 
-  private void openDataSet(String dataSetName) {
-    HopGui hopGui = HopGui.getInstance();
+  /**
+   * Resolve the output fields of a single transform for the dataset location 
dialog. The dialog
+   * only asks for the transform it's mapping fields for, which keeps a slow 
transform (a Table
+   * input needing a database connection for example) from blocking the whole 
dialog.
+   */
+  private Function<String, IRowMeta> transformFieldsResolver(
+      HopGuiPipelineGraph pipelineGraph, PipelineMeta pipelineMeta) {
+    return transformName -> {
+      try {
+        return pipelineMeta.getTransformFields(pipelineGraph.getVariables(), 
transformName);
+      } catch (Exception e) {
+        // Ignore GUI errors: the dialog reports unknown fields to the user.
+        //
+        return null;
+      }
+    };
+  }
+
+  private void showGoldenDataResult(
+      HopGuiPipelineGraph pipelineGraph, HopGui hopGui, PipelineUnitTest 
unitTest, String name) {
+    if (unitTest.findGoldenLocation(name) == null) {
+      return;
+    }
+
+    // Find the errors list of the unit test...
+    //
+    IPipelineEngine<PipelineMeta> pipeline = pipelineGraph.getPipeline();
+    if (pipeline == null) {
+      return;
+    }
+
+    List<UnitTestResult> results =
+        (List<UnitTestResult>) 
pipeline.getExtensionDataMap().get(DataSetConst.UNIT_TEST_RESULTS);
+    if (Utils.isEmpty(results)) {
+      return;
+    }
 
-    MetadataManager<DataSet> manager =
-        new MetadataManager<>(
-            hopGui.getVariables(), hopGui.getMetadataProvider(), 
DataSet.class, hopGui.getShell());
-    manager.editMetadata(dataSetName);
+    ValidatePipelineUnitTestExtensionPoint.showUnitTestErrors(pipeline, 
results, hopGui);
   }
 }
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/ui/testing/PipelineUnitTestSetLocationDialog.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/ui/testing/PipelineUnitTestSetLocationDialog.java
index dc22e044d9..b9c2c8079e 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/ui/testing/PipelineUnitTestSetLocationDialog.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/ui/testing/PipelineUnitTestSetLocationDialog.java
@@ -18,8 +18,10 @@
 package org.apache.hop.ui.testing;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hop.core.CheckResult;
 import org.apache.hop.core.Const;
@@ -63,7 +65,16 @@ public class PipelineUnitTestSetLocationDialog extends 
Dialog {
 
   private final PipelineUnitTestSetLocation location;
   private final List<DataSet> dataSets;
-  private final Map<String, IRowMeta> transformFieldsMap;
+
+  /**
+   * Resolves the output fields of a transform, or returns null when they 
can't be determined.
+   * Called on demand only: resolving the fields of every transform up front 
can be very expensive
+   * (issue #8203), for example when a Table input transform has to connect to 
a database for it.
+   */
+  private final Function<String, IRowMeta> transformFieldsResolver;
+
+  /** Cache of the fields resolved so far in this dialog, keyed by transform 
name. */
+  private final Map<String, IRowMeta> transformFieldsCache = new HashMap<>();
 
   private final String[] transformNames;
   private final String[] datasetNames;
@@ -87,17 +98,18 @@ public class PipelineUnitTestSetLocationDialog extends 
Dialog {
       IHopMetadataProvider metadataProvider,
       PipelineUnitTestSetLocation location,
       List<DataSet> dataSets,
-      Map<String, IRowMeta> transformFieldsMap) {
+      String[] transformNames,
+      Function<String, IRowMeta> transformFieldsResolver) {
     super(parent, SWT.NONE);
     this.variables = variables;
     this.metadataProvider = metadataProvider;
     this.location = location;
     this.dataSets = dataSets;
-    this.transformFieldsMap = transformFieldsMap;
+    this.transformFieldsResolver = transformFieldsResolver;
     props = PropsUi.getInstance();
     ok = false;
 
-    transformNames = transformFieldsMap.keySet().toArray(new String[0]);
+    this.transformNames = transformNames;
     datasetNames = new String[dataSets.size()];
     for (int i = 0; i < datasetNames.length; i++) {
       datasetNames[i] = dataSets.get(i).getName();
@@ -273,6 +285,21 @@ public class PipelineUnitTestSetLocationDialog extends 
Dialog {
     return ok;
   }
 
+  /**
+   * Get the output fields of the given transform, resolving them the first 
time they are needed.
+   *
+   * @param transformName the transform to get the fields for
+   * @return the fields, or null when they can't be determined
+   */
+  private IRowMeta getTransformFields(String transformName) {
+    // Remember failures as well (null values): retrying is just as expensive 
as the first attempt.
+    //
+    if (!transformFieldsCache.containsKey(transformName)) {
+      transformFieldsCache.put(transformName, 
transformFieldsResolver.apply(transformName));
+    }
+    return transformFieldsCache.get(transformName);
+  }
+
   protected void getFieldMappings() {
 
     try {
@@ -286,7 +313,7 @@ public class PipelineUnitTestSetLocationDialog extends 
Dialog {
         throw new HopException("Please select a transform and a data set to 
map fields between");
       }
 
-      IRowMeta transformRowMeta = transformFieldsMap.get(transformName);
+      IRowMeta transformRowMeta = getTransformFields(transformName);
       if (transformRowMeta == null) {
         throw new HopException("Unable to find fields for transform " + 
transformName);
       }
@@ -482,7 +509,7 @@ public class PipelineUnitTestSetLocationDialog extends 
Dialog {
     // Check fields of the transform if selected
     IRowMeta transformRowMeta = null;
     if (StringUtils.isNotEmpty(transformName)) {
-      transformRowMeta = transformFieldsMap.get(transformName);
+      transformRowMeta = getTransformFields(transformName);
       if (transformRowMeta == null) {
         remarks.add(
             new CheckResult(
diff --git 
a/plugins/misc/testing/src/test/java/org/apache/hop/ui/testing/PipelineUnitTestSetLocationDialogTest.java
 
b/plugins/misc/testing/src/test/java/org/apache/hop/ui/testing/PipelineUnitTestSetLocationDialogTest.java
new file mode 100644
index 0000000000..27dd62da8d
--- /dev/null
+++ 
b/plugins/misc/testing/src/test/java/org/apache/hop/ui/testing/PipelineUnitTestSetLocationDialogTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.testing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.testing.DataSet;
+import org.apache.hop.testing.DataSetField;
+import org.apache.hop.testing.PipelineUnitTestSetLocation;
+import org.apache.hop.ui.core.dialog.EnterMappingDialog;
+import org.eclipse.swtbot.swt.finder.SWTBot;
+import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Guards the design-time cost of the data set location dialog, the dialog 
opened from the unit test
+ * markers on the pipeline canvas.
+ *
+ * <p>It used to be handed a map holding the output fields of <em>every</em> 
transform in the
+ * pipeline, so opening it had to resolve them all first. A transform that 
determines its output
+ * from a variable - a Table input reading its connection from {@code 
${connection}} for example -
+ * connects to a database to answer that, on the SWT thread, which froze Hop 
GUI (issue #8203). The
+ * dialog now resolves the fields of the one transform it is mapping, and only 
when it needs them.
+ */
+@Tag("uitest")
+class PipelineUnitTestSetLocationDialogTest extends SwtBotTestBase {
+
+  private static final String MAPPED_TRANSFORM = "Fonte Sql";
+  private static final String DATA_SET_NAME = "raw rows";
+  private static final String[] TRANSFORM_NAMES = {
+    "Iniciar", "Log Inicial", MAPPED_TRANSFORM, "Salva S3", "Metricas Fluxo", 
"Log Final"
+  };
+
+  /** Counts how often the fields of a transform were resolved, keyed by 
transform name. */
+  private final Map<String, AtomicInteger> resolutions = new 
ConcurrentHashMap<>();
+
+  @Test
+  void openingTheDialogResolvesNoTransformFields() {
+    withDialog(
+        parent -> newDialog(parent).open(),
+        bot -> {
+          SWTBot dialogBot = locationShell(bot).bot();
+          // Nothing was asked for yet: the dialog is up without a single 
field lookup.
+          assertTrue(
+              resolutions.isEmpty(), "Opening the dialog resolved fields: " + 
resolutions.keySet());
+          dialogBot.button(buttonLabel("System.Button.Cancel")).click();
+        });
+
+    assertTrue(resolutions.isEmpty(), "Fields resolved: " + 
resolutions.keySet());
+  }
+
+  @Test
+  void fieldsOfTheMappedTransformAreResolvedOnceOnDemand() {
+    withDialog(
+        parent -> newDialog(parent).open(),
+        bot -> {
+          SWTBotShell locationShell = locationShell(bot);
+
+          // Map fields twice: the second round must be served from the 
dialog's own cache.
+          //
+          mapFieldsAndCancel(bot, locationShell);
+          mapFieldsAndCancel(bot, locationShell);
+
+          
locationShell.bot().button(buttonLabel("System.Button.Cancel")).click();
+        });
+
+    assertEquals(
+        List.of(MAPPED_TRANSFORM),
+        List.copyOf(resolutions.keySet()),
+        "Only the transform being mapped should have its fields resolved");
+    assertEquals(
+        1, resolutions.get(MAPPED_TRANSFORM).get(), "Resolved fields should be 
reused, not redone");
+  }
+
+  private void mapFieldsAndCancel(SWTBot bot, SWTBotShell locationShell) {
+    locationShell
+        .bot()
+        .button(
+            BaseMessages.getString(
+                PipelineUnitTestSetLocationDialog.class,
+                "PipelineUnitTestSetLocationDialog.MapFields.Button"))
+        .click();
+
+    SWTBotShell mappingShell =
+        bot.shell(BaseMessages.getString(EnterMappingDialog.class, 
"EnterMappingDialog.Title"));
+    mappingShell.activate();
+    mappingShell.bot().button(buttonLabel("System.Button.Cancel")).click();
+  }
+
+  private SWTBotShell locationShell(SWTBot bot) {
+    SWTBotShell shell =
+        bot.shell(
+            BaseMessages.getString(
+                PipelineUnitTestSetLocationDialog.class,
+                "PipelineUnitTestSetLocationDialog.Shell.Title"));
+    shell.activate();
+    return shell;
+  }
+
+  private PipelineUnitTestSetLocationDialog 
newDialog(org.eclipse.swt.widgets.Shell parent) {
+    IVariables variables = new Variables();
+    DataSet dataSet = dataSet();
+    IHopMetadataProvider metadataProvider = metadataProvider(dataSet);
+
+    PipelineUnitTestSetLocation location = new PipelineUnitTestSetLocation();
+    location.setTransformName(MAPPED_TRANSFORM);
+    location.setDataSetName(DATA_SET_NAME);
+
+    return new PipelineUnitTestSetLocationDialog(
+        parent,
+        variables,
+        metadataProvider,
+        location,
+        List.of(dataSet),
+        TRANSFORM_NAMES,
+        this::resolveFields);
+  }
+
+  /** Stands in for {@code PipelineMeta.getTransformFields()}, counting every 
call. */
+  private IRowMeta resolveFields(String transformName) {
+    resolutions.computeIfAbsent(transformName, name -> new 
AtomicInteger()).incrementAndGet();
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("id"));
+    rowMeta.addValueMeta(new ValueMetaString("name"));
+    return rowMeta;
+  }
+
+  private static DataSet dataSet() {
+    DataSet dataSet = new DataSet();
+    dataSet.setName(DATA_SET_NAME);
+    dataSet.getFields().add(new DataSetField("id", IValueMeta.TYPE_STRING, 50, 
-1, null, null));
+    dataSet.getFields().add(new DataSetField("name", IValueMeta.TYPE_STRING, 
50, -1, null, null));
+    return dataSet;
+  }
+
+  private static IHopMetadataProvider metadataProvider(DataSet dataSet) {
+    try {
+      MemoryMetadataProvider provider = new MemoryMetadataProvider();
+      provider.getSerializer(DataSet.class).save(dataSet);
+      return provider;
+    } catch (Exception e) {
+      throw new IllegalStateException("Unable to prepare the data set 
metadata", e);
+    }
+  }
+}
diff --git 
a/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputMeta.java
 
b/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputMeta.java
index 30dc063e60..882f789e56 100644
--- 
a/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputMeta.java
+++ 
b/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputMeta.java
@@ -180,6 +180,15 @@ public class TableInputMeta extends 
BaseTransformMeta<TableInput, TableInputData
           e);
     }
 
+    // The connection name can be a variable which isn't set at design time.  
Without a connection
+    // we can't determine any fields, so report it instead of failing with an 
NPE further down.
+    //
+    if (databaseMeta == null) {
+      throw new HopTransformException(
+          BaseMessages.getString(
+              PKG, "TableInputMeta.Exception.ConnectionNotFound", 
variables.resolve(connection)));
+    }
+
     Database db = new Database(loggingObject, variables, databaseMeta);
     super.databases = new Database[] {db}; // keep track of it for canceling 
purposes...
 
diff --git 
a/plugins/transforms/tableinput/src/main/resources/org/apache/hop/pipeline/transforms/tableinput/messages/messages_en_US.properties
 
b/plugins/transforms/tableinput/src/main/resources/org/apache/hop/pipeline/transforms/tableinput/messages/messages_en_US.properties
index 30d38565dc..93d4510ee1 100644
--- 
a/plugins/transforms/tableinput/src/main/resources/org/apache/hop/pipeline/transforms/tableinput/messages/messages_en_US.properties
+++ 
b/plugins/transforms/tableinput/src/main/resources/org/apache/hop/pipeline/transforms/tableinput/messages/messages_en_US.properties
@@ -101,5 +101,6 @@ TableInputSql.Exception.UnclosedNamedParameter=Unclosed 
named parameter in SQL.
 TableInputSql.Exception.EmptyNamedParameter=Empty named parameter in SQL.
 TableInputSql.Exception.MixedPlaceholders=SQL cannot mix named parameters with 
positional question-mark placeholders. Use one style only.
 TableInputMeta.Exception.CouldNotLoadSqlFromFile=Could not load SQL from file: 
{0}
+TableInputMeta.Exception.ConnectionNotFound=Unable to find database connection 
''{0}''.  Check the connection name, it may contain a variable which isn''t set.
 TableInputMeta.keyword=sql,query,database,select,jdbc
 System.FileType.AllFiles=All files
diff --git 
a/plugins/transforms/tableinput/src/test/java/org/apache/hop/pipeline/transforms/tableinput/TableInputMetaTest.java
 
b/plugins/transforms/tableinput/src/test/java/org/apache/hop/pipeline/transforms/tableinput/TableInputMetaTest.java
index 657734c41c..34b4e26f9d 100644
--- 
a/plugins/transforms/tableinput/src/test/java/org/apache/hop/pipeline/transforms/tableinput/TableInputMetaTest.java
+++ 
b/plugins/transforms/tableinput/src/test/java/org/apache/hop/pipeline/transforms/tableinput/TableInputMetaTest.java
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.Objects;
 import org.apache.hop.core.HopEnvironment;
 import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.exception.HopTransformException;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.RowMeta;
@@ -374,6 +375,29 @@ class TableInputMetaTest {
                         && r.getText().contains("3 fields")));
   }
 
+  @Test
+  void getFieldsReportsUnresolvedConnectionVariable() {
+    // A connection name which is a variable that isn't set at design time 
used to fail with a
+    // NullPointerException deep inside the Database object.  See issue #8203.
+    //
+    TableInputMeta meta = new TableInputMeta();
+    meta.setConnection("${connection_name}");
+    meta.setSql("SELECT * FROM t");
+
+    HopTransformException e =
+        Assertions.assertThrows(
+            HopTransformException.class,
+            () ->
+                meta.getFields(
+                    new RowMeta(),
+                    "Table input",
+                    null,
+                    null,
+                    new Variables(),
+                    new MemoryMetadataProvider()));
+    Assertions.assertTrue(e.getMessage().contains("${connection_name}"), 
e.getMessage());
+  }
+
   private static TableInputMeta namedParameterMeta() {
     TableInputMeta meta = new TableInputMeta();
     meta.setConnection("h2");

Reply via email to