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 f7ccb8ae55 Update info streams correctly across multiple transforms, 
fixes #4877 (#6591)
f7ccb8ae55 is described below

commit f7ccb8ae55490649e84c52aa8cbf84ac8147e540
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Feb 17 13:51:16 2026 +0100

    Update info streams correctly across multiple transforms, fixes #4877 
(#6591)
    
    * Update info streams correctly across multiple transforms, fixes #4877
    
    * spotless
---
 assemblies/debug/pom.xml                           |  30 +++++
 .../pipeline/transforms/append/AppendDialog.java   |  18 ++-
 .../hop/pipeline/transforms/append/AppendMeta.java | 114 +++++++++++++++-
 .../transforms/joinrows/JoinRowsDialog.java        |   3 +
 .../pipeline/transforms/joinrows/JoinRowsMeta.java |  93 ++++++++++++-
 .../joinrows/messages/messages_en_US.properties    |   1 +
 .../transforms/mergejoin/MergeJoinDialog.java      |  12 ++
 .../transforms/mergejoin/MergeJoinMeta.java        | 117 ++++++++++++++--
 .../multimerge/MultiMergeJoinDialog.java           |  14 ++
 .../transforms/multimerge/MultiMergeJoinMeta.java  | 148 +++++++++++++++++++--
 .../hopgui/file/pipeline/HopGuiPipelineGraph.java  |  38 +++++-
 .../delegates/HopGuiPipelineTransformDelegate.java |  11 +-
 12 files changed, 569 insertions(+), 30 deletions(-)

diff --git a/assemblies/debug/pom.xml b/assemblies/debug/pom.xml
index 2fea07e0f7..857efa8161 100644
--- a/assemblies/debug/pom.xml
+++ b/assemblies/debug/pom.xml
@@ -427,6 +427,12 @@
             <version>${project.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hop</groupId>
+            <artifactId>hop-transform-append</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.hop</groupId>
             <artifactId>hop-transform-blockuntiltransformsfinish</artifactId>
@@ -469,6 +475,24 @@
             <version>${project.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hop</groupId>
+            <artifactId>hop-transform-joinrows</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.hop</groupId>
+            <artifactId>hop-transform-mergejoin</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.hop</groupId>
+            <artifactId>hop-transform-multimerge</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.hop</groupId>
             <artifactId>hop-transform-pipelineexecutor</artifactId>
@@ -499,6 +523,12 @@
             <version>${project.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hop</groupId>
+            <artifactId>hop-transform-streamlookup</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.hop</groupId>
             <artifactId>hop-transform-textfile</artifactId>
diff --git 
a/plugins/transforms/append/src/main/java/org/apache/hop/pipeline/transforms/append/AppendDialog.java
 
b/plugins/transforms/append/src/main/java/org/apache/hop/pipeline/transforms/append/AppendDialog.java
index cc0fc39503..3090704998 100644
--- 
a/plugins/transforms/append/src/main/java/org/apache/hop/pipeline/transforms/append/AppendDialog.java
+++ 
b/plugins/transforms/append/src/main/java/org/apache/hop/pipeline/transforms/append/AppendDialog.java
@@ -17,11 +17,13 @@
 
 package org.apache.hop.pipeline.transforms.append;
 
+import java.util.List;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.stream.IStream;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.dialog.BaseDialog;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
@@ -160,9 +162,21 @@ public class AppendDialog extends BaseTransformDialog {
 
   /** Copy information from the meta-data input to the dialog fields. */
   public void getData() {
+    // If both fields are empty and exactly 2 transforms are attached, 
auto-fill head and tail
+    if (Utils.isEmpty(input.getHeadTransformName())
+        && Utils.isEmpty(input.getTailTransformName())) {
+      String[] prev = pipelineMeta.getPrevTransformNames(transformName);
+      if (prev != null && prev.length == 2) {
+        input.setHeadTransformName(prev[0]);
+        input.setTailTransformName(prev[1]);
+      }
+    }
+    // Sync from hops (rename, insert-in-the-middle) and resolve streams
+    input.searchInfoAndTargetTransforms(pipelineMeta.getTransforms());
 
-    wHeadHop.setText(Const.NVL(input.getHeadTransformName(), ""));
-    wTailHop.setText(Const.NVL(input.getTailTransformName(), ""));
+    List<IStream> infoStreams = input.getTransformIOMeta().getInfoStreams();
+    wHeadHop.setText(Const.NVL(infoStreams.get(0).getTransformName(), ""));
+    wTailHop.setText(Const.NVL(infoStreams.get(1).getTransformName(), ""));
 
     wTransformName.selectAll();
     wTransformName.setFocus();
diff --git 
a/plugins/transforms/append/src/main/java/org/apache/hop/pipeline/transforms/append/AppendMeta.java
 
b/plugins/transforms/append/src/main/java/org/apache/hop/pipeline/transforms/append/AppendMeta.java
index caa00ee9c7..0b04495328 100644
--- 
a/plugins/transforms/append/src/main/java/org/apache/hop/pipeline/transforms/append/AppendMeta.java
+++ 
b/plugins/transforms/append/src/main/java/org/apache/hop/pipeline/transforms/append/AppendMeta.java
@@ -18,11 +18,13 @@
 package org.apache.hop.pipeline.transforms.append;
 
 import java.util.List;
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.hop.core.CheckResult;
 import org.apache.hop.core.ICheckResult;
 import org.apache.hop.core.annotations.Transform;
 import org.apache.hop.core.exception.HopTransformException;
 import org.apache.hop.core.row.IRowMeta;
+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.HopMetadataProperty;
@@ -75,9 +77,115 @@ public class AppendMeta extends BaseTransformMeta<Append, 
AppendData> {
 
   @Override
   public void searchInfoAndTargetTransforms(List<TransformMeta> transforms) {
-    List<IStream> streams = getTransformIOMeta().getInfoStreams();
-    streams.get(0).setTransformMeta(TransformMeta.findTransform(transforms, 
headTransformName));
-    streams.get(1).setTransformMeta(TransformMeta.findTransform(transforms, 
tailTransformName));
+    List<IStream> infoStreams = getTransformIOMeta().getInfoStreams();
+    if (infoStreams.size() < 2) {
+      return;
+    }
+    IStream headStream = infoStreams.get(0);
+    IStream tailStream = infoStreams.get(1);
+    // Respect user choice: only re-fill from stream/prev when they had set a 
value (e.g. rename).
+    boolean hadHeadFilledIn = !Utils.isEmpty(headTransformName);
+    boolean hadTailFilledIn = !Utils.isEmpty(tailTransformName);
+
+    if (parentTransformMeta != null) {
+      PipelineMeta pipelineMeta = parentTransformMeta.getParentPipelineMeta();
+      if (pipelineMeta != null) {
+        String[] prev = 
pipelineMeta.getPrevTransformNames(parentTransformMeta);
+        // Only auto-fill when both are empty (initial connect). Do not 
re-fill when user cleared
+        // one.
+        if (prev != null && prev.length == 2) {
+          if (Utils.isEmpty(headTransformName) && 
Utils.isEmpty(tailTransformName)) {
+            if (headStream.getTransformMeta() != null && 
tailStream.getTransformMeta() != null) {
+              headTransformName = headStream.getTransformName();
+              tailTransformName = tailStream.getTransformName();
+            } else {
+              headTransformName = prev[0];
+              tailTransformName = prev[1];
+              setChanged();
+            }
+          }
+        }
+        // Clear names that no longer exist in prev (e.g. transform was 
removed)
+        if (headTransformName != null && !ArrayUtils.contains(prev, 
headTransformName)) {
+          headTransformName = null;
+          setChanged();
+        }
+        if (tailTransformName != null && !ArrayUtils.contains(prev, 
tailTransformName)) {
+          tailTransformName = null;
+          setChanged();
+        }
+      }
+    }
+
+    // Resolve by name. Prefer stream only when the stored name is "stale": 
empty (auto-fill),
+    // not in prev (insert-in-the-middle), or transform not found (rename).
+    String[] prev = null;
+    if (parentTransformMeta != null && 
parentTransformMeta.getParentPipelineMeta() != null) {
+      prev = 
parentTransformMeta.getParentPipelineMeta().getPrevTransformNames(parentTransformMeta);
+    }
+    TransformMeta tmHead = null;
+    boolean headNameStale =
+        Utils.isEmpty(headTransformName)
+            || (prev != null && !ArrayUtils.contains(prev, headTransformName))
+            || TransformMeta.findTransform(transforms, headTransformName) == 
null;
+    boolean preferHeadStream =
+        headStream.getTransformMeta() != null
+            && prev != null
+            && ArrayUtils.contains(prev, headStream.getTransformName())
+            && headNameStale
+            && hadHeadFilledIn;
+    if (preferHeadStream) {
+      headTransformName = headStream.getTransformName();
+      tmHead = headStream.getTransformMeta();
+      setChanged();
+    }
+    if (tmHead == null) {
+      tmHead = TransformMeta.findTransform(transforms, headTransformName);
+      if (tmHead == null
+          && headStream.getTransformMeta() != null
+          && prev != null
+          && ArrayUtils.contains(prev, headStream.getTransformName())
+          && hadHeadFilledIn) {
+        headTransformName = headStream.getTransformName();
+        tmHead = TransformMeta.findTransform(transforms, headTransformName);
+      }
+    }
+    headStream.setTransformMeta(tmHead);
+    if (tmHead != null) {
+      headStream.setSubject(tmHead.getName());
+    }
+
+    TransformMeta tmTail = null;
+    boolean tailNameStale =
+        Utils.isEmpty(tailTransformName)
+            || (prev != null && !ArrayUtils.contains(prev, tailTransformName))
+            || TransformMeta.findTransform(transforms, tailTransformName) == 
null;
+    boolean preferTailStream =
+        tailStream.getTransformMeta() != null
+            && prev != null
+            && ArrayUtils.contains(prev, tailStream.getTransformName())
+            && tailNameStale
+            && hadTailFilledIn;
+    if (preferTailStream) {
+      tailTransformName = tailStream.getTransformName();
+      tmTail = tailStream.getTransformMeta();
+      setChanged();
+    }
+    if (tmTail == null) {
+      tmTail = TransformMeta.findTransform(transforms, tailTransformName);
+      if (tmTail == null
+          && tailStream.getTransformMeta() != null
+          && prev != null
+          && ArrayUtils.contains(prev, tailStream.getTransformName())
+          && hadTailFilledIn) {
+        tailTransformName = tailStream.getTransformName();
+        tmTail = TransformMeta.findTransform(transforms, tailTransformName);
+      }
+    }
+    tailStream.setTransformMeta(tmTail);
+    if (tmTail != null) {
+      tailStream.setSubject(tmTail.getName());
+    }
   }
 
   @Override
diff --git 
a/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsDialog.java
 
b/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsDialog.java
index 9832eb816c..6cc4266eaf 100644
--- 
a/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsDialog.java
+++ 
b/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsDialog.java
@@ -259,6 +259,9 @@ public class JoinRowsDialog extends BaseTransformDialog {
 
   /** Copy information from the meta-data input to the dialog fields. */
   public void getData() {
+    // Sync from hops (rename, insert-in-the-middle) so main transform is up 
to date
+    input.searchInfoAndTargetTransforms(pipelineMeta.getTransforms());
+
     if (input.getPrefix() != null) {
       wPrefix.setText(input.getPrefix());
     }
diff --git 
a/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsMeta.java
 
b/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsMeta.java
index da075f8d01..08e6266860 100644
--- 
a/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsMeta.java
+++ 
b/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsMeta.java
@@ -21,6 +21,7 @@ import java.io.File;
 import java.util.List;
 import lombok.Getter;
 import lombok.Setter;
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.hop.core.CheckResult;
 import org.apache.hop.core.Condition;
 import org.apache.hop.core.ICheckResult;
@@ -28,6 +29,7 @@ import org.apache.hop.core.annotations.Transform;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.exception.HopTransformException;
 import org.apache.hop.core.row.IRowMeta;
+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.HopMetadataProperty;
@@ -35,7 +37,13 @@ import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.metadata.api.IStringObjectConverter;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.transform.BaseTransformMeta;
+import org.apache.hop.pipeline.transform.ITransformIOMeta;
+import org.apache.hop.pipeline.transform.TransformIOMeta;
 import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transform.stream.IStream;
+import org.apache.hop.pipeline.transform.stream.IStream.StreamType;
+import org.apache.hop.pipeline.transform.stream.Stream;
+import org.apache.hop.pipeline.transform.stream.StreamIcon;
 
 @Transform(
     id = "JoinRows",
@@ -216,12 +224,95 @@ public class JoinRowsMeta extends 
BaseTransformMeta<JoinRows, JoinRowsData> {
     return null;
   }
 
+  /**
+   * Returns the I/O meta with one INFO stream when "Main transform to read 
from" is set (like
+   * Stream Lookup).
+   */
+  @Override
+  public ITransformIOMeta getTransformIOMeta() {
+    ITransformIOMeta ioMeta = super.getTransformIOMeta(false);
+    if (ioMeta == null) {
+      ioMeta = new TransformIOMeta(true, true, false, false, false, false);
+      IStream stream =
+          new Stream(
+              StreamType.INFO,
+              null,
+              BaseMessages.getString(PKG, 
"JoinRowsMeta.InfoStream.Description"),
+              StreamIcon.INFO,
+              mainTransformName);
+      ioMeta.addStream(stream);
+      setTransformIOMeta(ioMeta);
+    }
+    return ioMeta;
+  }
+
+  @Override
+  public void resetTransformIoMeta() {
+    // Don't reset
+  }
+
   /**
    * @param transforms optionally search the info transform in a list of 
transforms
    */
   @Override
   public void searchInfoAndTargetTransforms(List<TransformMeta> transforms) {
-    mainTransform = TransformMeta.findTransform(transforms, mainTransformName);
+    List<IStream> infoStreams = getTransformIOMeta().getInfoStreams();
+    if (infoStreams.isEmpty()) {
+      mainTransform = TransformMeta.findTransform(transforms, 
mainTransformName);
+      return;
+    }
+    IStream stream = infoStreams.get(0);
+
+    String[] prev = null;
+    if (parentTransformMeta != null && 
parentTransformMeta.getParentPipelineMeta() != null) {
+      prev = 
parentTransformMeta.getParentPipelineMeta().getPrevTransformNames(parentTransformMeta);
+    }
+
+    // Clear name when no longer in prev (transform removed)
+    if (prev != null
+        && mainTransformName != null
+        && !ArrayUtils.contains(prev, mainTransformName)
+        && (stream.getTransformMeta() == null
+            || !ArrayUtils.contains(prev, stream.getTransformName()))) {
+      mainTransformName = null;
+      setChanged();
+    }
+
+    // Resolve: prefer stream when name is stale (rename / 
insert-in-the-middle / detach).
+    // Do not re-fill from stream when mainTransformName is empty (user chose 
to clear / no main).
+    boolean nameStale =
+        Utils.isEmpty(mainTransformName)
+            || (prev != null && !ArrayUtils.contains(prev, mainTransformName))
+            || TransformMeta.findTransform(transforms, mainTransformName) == 
null;
+    boolean preferStream =
+        stream.getTransformMeta() != null
+            && prev != null
+            && ArrayUtils.contains(prev, stream.getTransformName())
+            && nameStale
+            && !Utils.isEmpty(mainTransformName);
+
+    TransformMeta tm = null;
+    if (preferStream) {
+      mainTransformName = stream.getTransformName();
+      mainTransform = stream.getTransformMeta();
+      tm = mainTransform;
+      setChanged();
+    }
+    if (tm == null) {
+      tm = TransformMeta.findTransform(transforms, mainTransformName);
+      if (tm == null && !Utils.isEmpty(mainTransformName) && 
stream.getTransformMeta() != null) {
+        mainTransformName = stream.getTransformName();
+        tm = TransformMeta.findTransform(transforms, mainTransformName);
+        setChanged();
+      }
+      mainTransform = tm;
+    }
+    stream.setTransformMeta(tm);
+    if (tm != null) {
+      stream.setSubject(tm.getName());
+    } else {
+      stream.setSubject(null);
+    }
   }
 
   @Override
diff --git 
a/plugins/transforms/joinrows/src/main/resources/org/apache/hop/pipeline/transforms/joinrows/messages/messages_en_US.properties
 
b/plugins/transforms/joinrows/src/main/resources/org/apache/hop/pipeline/transforms/joinrows/messages/messages_en_US.properties
index 09975c0af7..17e3343c02 100644
--- 
a/plugins/transforms/joinrows/src/main/resources/org/apache/hop/pipeline/transforms/joinrows/messages/messages_en_US.properties
+++ 
b/plugins/transforms/joinrows/src/main/resources/org/apache/hop/pipeline/transforms/joinrows/messages/messages_en_US.properties
@@ -45,6 +45,7 @@ JoinRowsDialog.Temp.Label=temp
 JoinRowsDialog.TempDir.Label=Temp directory
 JoinRowsDialog.TempFilePrefix.Label=TMP-file prefix
 JoinRowsDialog.TransformName.Label=Transform name
+JoinRowsMeta.InfoStream.Description=Main transform to read from
 JoinRowsMeta.CheckResult.CouldNotFindFieldsFromPreviousTransforms=Couldn''t 
find fields from previous transforms, check the hops...\!
 JoinRowsMeta.CheckResult.DirectoryDoesNotExist=Directory [{0}] doesn''t exist!
 JoinRowsMeta.CheckResult.DirectoryExists=] exists and is a directory
diff --git 
a/plugins/transforms/mergejoin/src/main/java/org/apache/hop/pipeline/transforms/mergejoin/MergeJoinDialog.java
 
b/plugins/transforms/mergejoin/src/main/java/org/apache/hop/pipeline/transforms/mergejoin/MergeJoinDialog.java
index 3bd8f22ad4..1d024a77a0 100644
--- 
a/plugins/transforms/mergejoin/src/main/java/org/apache/hop/pipeline/transforms/mergejoin/MergeJoinDialog.java
+++ 
b/plugins/transforms/mergejoin/src/main/java/org/apache/hop/pipeline/transforms/mergejoin/MergeJoinDialog.java
@@ -297,6 +297,18 @@ public class MergeJoinDialog extends BaseTransformDialog {
 
   /** Copy information from the meta-data input to the dialog fields. */
   public void getData() {
+    // If both fields are empty and exactly 2 transforms are attached, 
auto-fill first and second
+    if (Utils.isEmpty(input.getLeftTransformName())
+        && Utils.isEmpty(input.getRightTransformName())) {
+      String[] prev = pipelineMeta.getPrevTransformNames(transformName);
+      if (prev != null && prev.length == 2) {
+        input.setLeftTransformName(prev[0]);
+        input.setRightTransformName(prev[1]);
+      }
+    }
+    // Sync from hops (rename, insert-in-the-middle) and resolve streams
+    input.searchInfoAndTargetTransforms(pipelineMeta.getTransforms());
+
     List<IStream> infoStreams = input.getTransformIOMeta().getInfoStreams();
 
     wTransform1.setText(Const.NVL(infoStreams.get(0).getTransformName(), ""));
diff --git 
a/plugins/transforms/mergejoin/src/main/java/org/apache/hop/pipeline/transforms/mergejoin/MergeJoinMeta.java
 
b/plugins/transforms/mergejoin/src/main/java/org/apache/hop/pipeline/transforms/mergejoin/MergeJoinMeta.java
index 763afc92d0..709d321157 100644
--- 
a/plugins/transforms/mergejoin/src/main/java/org/apache/hop/pipeline/transforms/mergejoin/MergeJoinMeta.java
+++ 
b/plugins/transforms/mergejoin/src/main/java/org/apache/hop/pipeline/transforms/mergejoin/MergeJoinMeta.java
@@ -121,22 +121,119 @@ public class MergeJoinMeta extends 
BaseTransformMeta<MergeJoin, MergeJoinData> {
   @Override
   public void searchInfoAndTargetTransforms(List<TransformMeta> transforms) {
     List<IStream> infoStreams = getTransformIOMeta().getInfoStreams();
+    if (infoStreams.size() < 2) {
+      return;
+    }
+    IStream stream0 = infoStreams.get(0);
+    IStream stream1 = infoStreams.get(1);
+    // Respect user choice: only re-fill from stream/prev when they had set a 
value (e.g. rename).
+    boolean hadLeftFilledIn = !Utils.isEmpty(leftTransformName);
+    boolean hadRightFilledIn = !Utils.isEmpty(rightTransformName);
+
     if (parentTransformMeta != null) {
-      String[] prev =
-          
parentTransformMeta.getParentPipelineMeta().getPrevTransformNames(parentTransformMeta);
-      if (leftTransformName != null && !ArrayUtils.contains(prev, 
leftTransformName)) {
-        leftTransformName = null;
+      PipelineMeta pipelineMeta = parentTransformMeta.getParentPipelineMeta();
+      if (pipelineMeta != null) {
+        String[] prev = 
pipelineMeta.getPrevTransformNames(parentTransformMeta);
+        // Only auto-fill when both are empty (initial connect). Do not 
re-fill when user cleared
+        // one.
+        if (prev != null && prev.length == 2) {
+          if (Utils.isEmpty(leftTransformName) && 
Utils.isEmpty(rightTransformName)) {
+            if (stream0.getTransformMeta() != null && 
stream1.getTransformMeta() != null) {
+              leftTransformName = stream0.getTransformName();
+              rightTransformName = stream1.getTransformName();
+            } else {
+              leftTransformName = prev[0];
+              rightTransformName = prev[1];
+              setChanged();
+            }
+          }
+        }
+        // Clear names that no longer exist in prev (e.g. transform was 
removed / detached)
+        if (leftTransformName != null && !ArrayUtils.contains(prev, 
leftTransformName)) {
+          leftTransformName = null;
+          setChanged();
+        }
+        if (rightTransformName != null && !ArrayUtils.contains(prev, 
rightTransformName)) {
+          rightTransformName = null;
+          setChanged();
+        }
+      }
+    }
+
+    // Resolve by name. Prefer stream only when the stored name is "stale": 
empty (auto-fill),
+    // not in prev (insert-in-the-middle), or transform not found (rename). Do 
not prefer when
+    // the user has set a valid name (e.g. swapped order) so we don't 
overwrite their choice.
+    String[] prev = null;
+    if (parentTransformMeta != null && 
parentTransformMeta.getParentPipelineMeta() != null) {
+      prev = 
parentTransformMeta.getParentPipelineMeta().getPrevTransformNames(parentTransformMeta);
+    }
+    TransformMeta tm0 = null;
+    boolean name0Stale =
+        Utils.isEmpty(leftTransformName)
+            || (prev != null && !ArrayUtils.contains(prev, leftTransformName))
+            || TransformMeta.findTransform(transforms, leftTransformName) == 
null;
+    boolean preferStream0 =
+        stream0.getTransformMeta() != null
+            && prev != null
+            && ArrayUtils.contains(prev, stream0.getTransformName())
+            && name0Stale
+            && hadLeftFilledIn;
+    if (preferStream0) {
+      leftTransformName = stream0.getTransformName();
+      tm0 = stream0.getTransformMeta();
+      setChanged();
+    }
+    if (tm0 == null) {
+      tm0 = TransformMeta.findTransform(transforms, leftTransformName);
+      // Only use stream as fallback when stream's transform is still in prev 
(e.g. rename). Do not
+      // re-apply stream when it points to a detached/removed transform.
+      if (tm0 == null
+          && stream0.getTransformMeta() != null
+          && prev != null
+          && ArrayUtils.contains(prev, stream0.getTransformName())
+          && hadLeftFilledIn) {
+        leftTransformName = stream0.getTransformName();
+        tm0 = TransformMeta.findTransform(transforms, leftTransformName);
         setChanged();
       }
-      if (rightTransformName != null && !ArrayUtils.contains(prev, 
rightTransformName)) {
-        rightTransformName = null;
+    }
+    stream0.setTransformMeta(tm0);
+    if (tm0 != null) {
+      stream0.setSubject(tm0.getName());
+    }
+
+    TransformMeta tm1 = null;
+    boolean name1Stale =
+        Utils.isEmpty(rightTransformName)
+            || (prev != null && !ArrayUtils.contains(prev, rightTransformName))
+            || TransformMeta.findTransform(transforms, rightTransformName) == 
null;
+    boolean preferStream1 =
+        stream1.getTransformMeta() != null
+            && prev != null
+            && ArrayUtils.contains(prev, stream1.getTransformName())
+            && name1Stale
+            && hadRightFilledIn;
+    if (preferStream1) {
+      rightTransformName = stream1.getTransformName();
+      tm1 = stream1.getTransformMeta();
+      setChanged();
+    }
+    if (tm1 == null) {
+      tm1 = TransformMeta.findTransform(transforms, rightTransformName);
+      if (tm1 == null
+          && stream1.getTransformMeta() != null
+          && prev != null
+          && ArrayUtils.contains(prev, stream1.getTransformName())
+          && hadRightFilledIn) {
+        rightTransformName = stream1.getTransformName();
+        tm1 = TransformMeta.findTransform(transforms, rightTransformName);
         setChanged();
       }
     }
-    
infoStreams.get(0).setTransformMeta(TransformMeta.findTransform(transforms, 
leftTransformName));
-    infoStreams
-        .get(1)
-        .setTransformMeta(TransformMeta.findTransform(transforms, 
rightTransformName));
+    stream1.setTransformMeta(tm1);
+    if (tm1 != null) {
+      stream1.setSubject(tm1.getName());
+    }
   }
 
   @Override
diff --git 
a/plugins/transforms/multimerge/src/main/java/org/apache/hop/pipeline/transforms/multimerge/MultiMergeJoinDialog.java
 
b/plugins/transforms/multimerge/src/main/java/org/apache/hop/pipeline/transforms/multimerge/MultiMergeJoinDialog.java
index 45583b643e..8e539336a9 100644
--- 
a/plugins/transforms/multimerge/src/main/java/org/apache/hop/pipeline/transforms/multimerge/MultiMergeJoinDialog.java
+++ 
b/plugins/transforms/multimerge/src/main/java/org/apache/hop/pipeline/transforms/multimerge/MultiMergeJoinDialog.java
@@ -412,6 +412,20 @@ public class MultiMergeJoinDialog extends 
BaseTransformDialog {
 
   /** Copy information from the meta-data input to the dialog fields. */
   public void getData() {
+    // If no inputs configured and at least 2 transforms are attached, 
auto-fill from prev
+    if (joinMeta.getInputTransforms() == null || 
joinMeta.getInputTransforms().isEmpty()) {
+      String[] prev = pipelineMeta.getPrevTransformNames(transformName);
+      if (prev != null && prev.length >= 2) {
+        List<String> list = new ArrayList<>();
+        for (String p : prev) {
+          list.add(p);
+        }
+        joinMeta.setInputTransforms(list);
+      }
+    }
+    // Sync from hops (rename, insert-in-the-middle) and resolve streams
+    joinMeta.searchInfoAndTargetTransforms(pipelineMeta.getTransforms());
+
     List<String> inputTransformNames = joinMeta.getInputTransforms();
     if (inputTransformNames != null) {
       String inputTransformName;
diff --git 
a/plugins/transforms/multimerge/src/main/java/org/apache/hop/pipeline/transforms/multimerge/MultiMergeJoinMeta.java
 
b/plugins/transforms/multimerge/src/main/java/org/apache/hop/pipeline/transforms/multimerge/MultiMergeJoinMeta.java
index 5da674d066..3fcfd1428a 100644
--- 
a/plugins/transforms/multimerge/src/main/java/org/apache/hop/pipeline/transforms/multimerge/MultiMergeJoinMeta.java
+++ 
b/plugins/transforms/multimerge/src/main/java/org/apache/hop/pipeline/transforms/multimerge/MultiMergeJoinMeta.java
@@ -21,12 +21,14 @@ import java.util.ArrayList;
 import java.util.List;
 import lombok.Getter;
 import lombok.Setter;
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.hop.core.CheckResult;
 import org.apache.hop.core.ICheckResult;
 import org.apache.hop.core.annotations.Transform;
 import org.apache.hop.core.exception.HopTransformException;
 import org.apache.hop.core.exception.HopXmlException;
 import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.xml.XmlHandler;
 import org.apache.hop.i18n.BaseMessages;
@@ -35,7 +37,9 @@ import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.transform.BaseTransformMeta;
 import org.apache.hop.pipeline.transform.ITransformIOMeta;
+import org.apache.hop.pipeline.transform.TransformIOMeta;
 import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transform.stream.IStream;
 import org.apache.hop.pipeline.transform.stream.IStream.StreamType;
 import org.apache.hop.pipeline.transform.stream.Stream;
 import org.apache.hop.pipeline.transform.stream.StreamIcon;
@@ -126,19 +130,145 @@ public class MultiMergeJoinMeta extends 
BaseTransformMeta<MultiMergeJoin, MultiM
     joinType = joinTypes[0];
   }
 
+  /**
+   * Returns the I/O meta with INFO streams so the pipeline marks input hops 
as info streams (like
+   * Merge Join / Append).
+   */
+  @Override
+  public ITransformIOMeta getTransformIOMeta() {
+    ITransformIOMeta ioMeta = super.getTransformIOMeta(false);
+    if (ioMeta == null) {
+      ioMeta = new TransformIOMeta(true, true, false, false, false, false);
+      int n = (inputTransforms != null && !inputTransforms.isEmpty()) ? 
inputTransforms.size() : 2;
+      for (int i = 0; i < n; i++) {
+        ioMeta.addStream(
+            new Stream(
+                StreamType.INFO,
+                null,
+                BaseMessages.getString(PKG, 
"MultiMergeJoin.InfoStream.Description"),
+                StreamIcon.INFO,
+                null));
+      }
+      setTransformIOMeta(ioMeta);
+    }
+    return ioMeta;
+  }
+
   @Override
   public void searchInfoAndTargetTransforms(List<TransformMeta> transforms) {
     ITransformIOMeta ioMeta = getTransformIOMeta();
-    ioMeta.getInfoStreams().clear();
+    List<IStream> infoStreams = ioMeta.getInfoStreams();
+
+    String[] prev = null;
+    if (parentTransformMeta != null && 
parentTransformMeta.getParentPipelineMeta() != null) {
+      prev = 
parentTransformMeta.getParentPipelineMeta().getPrevTransformNames(parentTransformMeta);
+    }
+
+    // Auto-fill when empty and we have connected transforms
+    if ((inputTransforms == null || inputTransforms.isEmpty())
+        && prev != null
+        && prev.length >= 2) {
+      inputTransforms = new ArrayList<>();
+      for (String p : prev) {
+        inputTransforms.add(p);
+      }
+      setChanged();
+    }
+    if (inputTransforms == null) {
+      inputTransforms = new ArrayList<>();
+    }
+
+    // Clear names that no longer exist in prev; keep and update name when 
it's a rename (stream's
+    // transform is in prev)
+    if (prev != null) {
+      List<String> newInputTransforms = new ArrayList<>();
+      List<String> newKeyFields = (keyFields != null) ? new ArrayList<>() : 
null;
+      for (int i = 0; i < inputTransforms.size(); i++) {
+        String name = inputTransforms.get(i);
+        if (Utils.isEmpty(name) || ArrayUtils.contains(prev, name)) {
+          newInputTransforms.add(name);
+          if (newKeyFields != null && i < keyFields.size()) {
+            newKeyFields.add(keyFields.get(i));
+          }
+        } else if (i < infoStreams.size()) {
+          IStream stream = infoStreams.get(i);
+          if (stream.getTransformMeta() != null
+              && ArrayUtils.contains(prev, stream.getTransformName())) {
+            // Renamed: keep entry with updated name
+            newInputTransforms.add(stream.getTransformName());
+            if (newKeyFields != null && i < keyFields.size()) {
+              newKeyFields.add(keyFields.get(i));
+            }
+            setChanged();
+          }
+        } else {
+          setChanged();
+        }
+      }
+      inputTransforms.clear();
+      inputTransforms.addAll(newInputTransforms);
+      if (keyFields != null) {
+        keyFields.clear();
+        keyFields.addAll(newKeyFields);
+      }
+    }
+
+    // Resolve each slot and build the list of streams to set 
(getInfoStreams() returns a copy, so
+    // we must replace via clearStreams + addStream)
+    List<IStream> resolvedStreams = new ArrayList<>();
+    String streamDescription = BaseMessages.getString(PKG, 
"MultiMergeJoin.InfoStream.Description");
+
     for (int i = 0; i < inputTransforms.size(); i++) {
-      String inputTransformName = inputTransforms.get(i);
-      ioMeta.addStream(
-          new Stream(
-              StreamType.INFO,
-              TransformMeta.findTransform(transforms, inputTransformName),
-              BaseMessages.getString(PKG, 
"MultiMergeJoin.InfoStream.Description"),
-              StreamIcon.INFO,
-              inputTransformName));
+      String name = inputTransforms.get(i);
+      IStream existingStream = (i < infoStreams.size()) ? infoStreams.get(i) : 
null;
+
+      boolean nameStale =
+          Utils.isEmpty(name)
+              || (prev != null && !ArrayUtils.contains(prev, name))
+              || TransformMeta.findTransform(transforms, name) == null;
+      boolean preferStream =
+          existingStream != null
+              && existingStream.getTransformMeta() != null
+              && prev != null
+              && ArrayUtils.contains(prev, existingStream.getTransformName())
+              && nameStale;
+
+      TransformMeta tm = null;
+      if (preferStream) {
+        name = existingStream.getTransformName();
+        inputTransforms.set(i, name);
+        tm = existingStream.getTransformMeta();
+        setChanged();
+      }
+      if (tm == null) {
+        tm = TransformMeta.findTransform(transforms, name);
+        if (tm == null && existingStream != null && 
existingStream.getTransformMeta() != null) {
+          name = existingStream.getTransformName();
+          inputTransforms.set(i, name);
+          tm = TransformMeta.findTransform(transforms, name);
+        }
+      }
+      String subject = (tm != null) ? tm.getName() : null;
+      resolvedStreams.add(
+          new Stream(StreamType.INFO, tm, streamDescription, StreamIcon.INFO, 
subject));
+    }
+
+    // Sync keyFields size
+    if (keyFields != null) {
+      while (keyFields.size() > inputTransforms.size()) {
+        keyFields.remove(keyFields.size() - 1);
+      }
+      while (keyFields.size() < inputTransforms.size()) {
+        keyFields.add("");
+      }
+    }
+
+    // Replace ioMeta streams so the pipeline sees INFO streams (like Merge 
Join / Append)
+    if (ioMeta instanceof TransformIOMeta) {
+      ((TransformIOMeta) ioMeta).clearStreams();
+      for (IStream s : resolvedStreams) {
+        ioMeta.addStream(s);
+      }
     }
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index 70d83168aa..098715e909 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -2351,12 +2351,12 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     PipelineHopMeta fromHop = pipelineMeta.findPipelineHopTo(transformMeta);
     PipelineHopMeta toHop = pipelineMeta.findPipelineHopFrom(transformMeta);
 
+    List<PipelineHopMeta> removedHops = new ArrayList<>();
     for (int i = pipelineMeta.nrPipelineHops() - 1; i >= 0; i--) {
       PipelineHopMeta hop = pipelineMeta.getPipelineHop(i);
       if (transformMeta.equals(hop.getFromTransform())
           || transformMeta.equals(hop.getToTransform())) {
-        // Transform is connected with a hop, remove this hop.
-        //
+        removedHops.add(hop);
         hopGui.undoDelegate.addUndoNew(pipelineMeta, new PipelineHopMeta[] 
{hop}, new int[] {i});
         pipelineMeta.removePipelineHop(i);
       }
@@ -2364,9 +2364,41 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
 
     // If the transform was part of a chain, re-connect it.
     //
+    TransformMeta newUpstream = null;
     if (fromHop != null && toHop != null) {
+      newUpstream = fromHop.getFromTransform();
       pipelineHopDelegate.newHop(
-          pipelineMeta, new PipelineHopMeta(fromHop.getFromTransform(), 
toHop.getToTransform()));
+          pipelineMeta, new PipelineHopMeta(newUpstream, 
toHop.getToTransform()));
+
+      // Same as split hop (insertTransform) but in reverse: point the 
target's info streams that
+      // were reading from the detached transform to the new upstream so
+      // searchInfoAndTargetTransforms
+      // can resolve correctly.
+      TransformMeta targetTransform = toHop.getToTransform();
+      if (targetTransform != null
+          && targetTransform.getTransform() != null
+          && newUpstream != null) {
+        ITransformIOMeta toIo = 
targetTransform.getTransform().getTransformIOMeta();
+        if (toIo != null) {
+          for (IStream stream : toIo.getInfoStreams()) {
+            if (stream.getTransformMeta() != null
+                && stream.getTransformMeta().equals(transformMeta)) {
+              stream.setTransformMeta(newUpstream);
+              stream.setSubject(newUpstream.getName());
+              targetTransform.getTransform().handleStreamSelection(stream);
+            }
+          }
+        }
+      }
+    }
+
+    // Same as split hop (insertTransform): after topology change, trigger IO 
meta update on
+    // targets of removed hops so linked transform names are updated.
+    for (PipelineHopMeta hop : removedHops) {
+      TransformMeta toTransform = hop.getToTransform();
+      if (toTransform != null && toTransform.getTransform() != null) {
+        
toTransform.getTransform().searchInfoAndTargetTransforms(pipelineMeta.getTransforms());
+      }
     }
 
     updateGui();
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
index a5b77c5fae..f962e743b0 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
@@ -474,6 +474,8 @@ public class HopGuiPipelineTransformDelegate {
       if (stream.getTransformMeta() != null && 
stream.getTransformMeta().equals(toTransform)) {
         // This target stream was directed to B, now we need to direct it to C
         stream.setTransformMeta(transformMeta);
+        // Update subject so searchInfoAndTargetTransforms resolves to C
+        stream.setSubject(transformMeta.getName());
         fromTransform.getTransform().handleStreamSelection(stream);
       }
     }
@@ -483,8 +485,10 @@ public class HopGuiPipelineTransformDelegate {
     ITransformIOMeta toIo = toTransform.getTransform().getTransformIOMeta();
     for (IStream stream : toIo.getInfoStreams()) {
       if (stream.getTransformMeta() != null && 
stream.getTransformMeta().equals(fromTransform)) {
-        // This info stream was reading from B, now we need to direct it to C
+        // This info stream was reading from A, now we need to direct it to C
         stream.setTransformMeta(transformMeta);
+        // Update subject so searchInfoAndTargetTransforms (e.g. Stream 
Lookup) resolves to C
+        stream.setSubject(transformMeta.getName());
         toTransform.getTransform().handleStreamSelection(stream);
       }
     }
@@ -512,7 +516,6 @@ public class HopGuiPipelineTransformDelegate {
     newHop2.setEnabled(hop.isEnabled());
     if (pipelineMeta.findPipelineHop(newHop2) == null) {
       pipelineMeta.addPipelineHop(newHop2);
-      
toTransform.getTransform().searchInfoAndTargetTransforms(pipelineMeta.getTransforms());
       hopGui.undoDelegate.addUndoNew(
           pipelineMeta,
           new PipelineHopMeta[] {newHop2},
@@ -520,6 +523,8 @@ public class HopGuiPipelineTransformDelegate {
           true);
     }
 
+    // Remove old hop before searchInfoAndTargetTransforms so "prev" reflects 
new topology
+    // (e.g. Merge Join's info stream can detect insert-in-the-middle).
     hopGui.undoDelegate.addUndoDelete(
         pipelineMeta,
         new PipelineHopMeta[] {hop},
@@ -527,6 +532,8 @@ public class HopGuiPipelineTransformDelegate {
         true);
     pipelineMeta.removePipelineHop(hop);
 
+    
toTransform.getTransform().searchInfoAndTargetTransforms(pipelineMeta.getTransforms());
+
     return transformMeta;
   }
 


Reply via email to