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

bamaer 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 c6b2ffe78f Issue #3460 : Add hash-code checksum and read it in 
Combination lookup (#8681)
c6b2ffe78f is described below

commit c6b2ffe78fc1c0a3f0ba95798b0359e5ebfc9ebb
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 09:11:09 2026 +0200

    Issue #3460 : Add hash-code checksum and read it in Combination lookup 
(#8681)
    
    * Issue #3460 : Add hash-code checksum and read it in Combination lookup
    
    * Issue #3460 : Point checksum documentation links at the transform pages
---
 .../pages/pipeline/transforms/addchecksum.adoc     |  11 +-
 .../pipeline/transforms/combinationlookup.adoc     |   6 +
 .../hop/pipeline/transforms/checksum/CheckSum.java |  27 +++-
 .../transforms/checksum/CheckSumDialog.java        |  34 +++--
 .../pipeline/transforms/checksum/CheckSumMeta.java |  13 +-
 .../checksum/messages/messages_en_US.properties    |   4 +-
 .../transforms/checksum/CheckSumMetaTest.java      |  26 ++++
 .../pipeline/transforms/checksum/CheckSumTest.java |  28 ++++
 .../combinationlookup/CombinationLookup.java       |  24 ++-
 .../combinationlookup/CombinationLookupData.java   |   1 +
 .../combinationlookup/CombinationLookupDialog.java |  46 +++++-
 .../combinationlookup/CombinationLookupMeta.java   |  46 ++++++
 .../messages/messages_en_US.properties             |   6 +
 .../CombinationLookupHashTest.java                 | 167 +++++++++++++++++++++
 .../CombinationLookupMetaTest.java                 |  28 +++-
 15 files changed, 434 insertions(+), 33 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addchecksum.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addchecksum.adoc
index cb7e71508c..1806e7041d 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addchecksum.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addchecksum.adoc
@@ -40,14 +40,22 @@ These are the types that are available:
 * MD5 : Message Digest algorithm 5 : http://en.wikipedia.org/wiki/MD5
 * SHA-1 : Secure Hash Algorithm 1 : http://en.wikipedia.org/wiki/SHA-1
 * SHA-256 : Secure Hash Algorithm 2 : http://en.wikipedia.org/wiki/SHA-256
-|Result Type|Some checksum types allow to set different result types: String, 
Hexadecimal and Binary
+* Hash code : The same signed integer that 
xref:pipeline/transforms/combinationlookup.adoc[Combination lookup/update] 
calculates from the native field values.
+The result is that 32-bit hash stored in a 64-bit integer, so negative values 
are possible.
+Nulls are included and field order matters.
+Prefix, separator and suffix are ignored, because they are not part of the 
combination-lookup calculation.
+|Result Type|Some checksum types allow to set different result types: String, 
Hexadecimal and Binary.
+CRC32, ADLER32 and Hash code always return an Integer. Result type does not 
apply to them.
 |Result field|The name of the result field containing the checksum
 |Prefix|An optional string to prepend before the field values when building 
the input for the checksum calculation.
 This is useful to distinguish hash keys from different topics, for example 
`CUST` for customer keys and `PROD` for product keys.
+Ignored for Hash code.
 |Separator|An optional string to insert between the field values when building 
the input for the checksum calculation.
 This helps generate more unique hash codes when multiple fields are 
concatenated.
+Ignored for Hash code.
 |Suffix|An optional string to append after the field values when building the 
input for the checksum calculation.
 Like the prefix, this can be used to distinguish hash keys from different 
topics.
+Ignored for Hash code.
 |Fields used in the checksum|The names of the fields to include in the 
checksum calculation.
 Note: You can use the "Get Fields" button to insert all input fields from 
previous transforms.
 |===
@@ -55,4 +63,5 @@ Note: You can use the "Get Fields" button to insert all input 
fields from previo
 == See also
 
 * xref:how-to-guides/cdc-snapshot-comparison.adoc[CDC: comparing snapshots] — 
storing an MD5 or SHA hash of a row and comparing it on the next run
+* xref:pipeline/transforms/combinationlookup.adoc[Combination lookup/update] — 
store this hash code on a junk dimension, or let that transform read a hash you 
calculated here
 
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/combinationlookup.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/combinationlookup.adoc
index 55507e4dd8..72f498ca47 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/combinationlookup.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/combinationlookup.adoc
@@ -73,6 +73,12 @@ Note that the new maximum is always cached, so that the 
maximum does not need to
 The only extra field added is then the technical key.
 |Use hashcode|This option allows you to generate a hash code, representing all 
values in the key fields in a numerical form (a signed 64 bit integer).
 This hash code has to be stored in the table.
+The key fields are still compared, so a hash collision does not return a 
different row.
+|Hashcode field in table|Name of the table column that stores the hash code.
+|Hash field in stream|Optional Integer field calculated earlier, for example 
by xref:pipeline/transforms/addchecksum.adoc[Add a checksum].
+When set, that value is written to the hashcode field and used for the lookup 
instead of calculating the hash in this transform.
+Leave it empty to keep calculating the hash from the key fields.
+To reproduce that calculated value, use Add a checksum with type Hash code on 
the same key fields in the same order.
 |Date of last update field|When required, specify the date of last update 
field (timestamp) from the source system to be copied to the data warehouse.
 For example, when you have an address without a primary key.
 The field will not be part of the lookup fields (nor be part in the hash code 
calculation).
diff --git 
a/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSum.java
 
b/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSum.java
index 0522333aff..e528714ff5 100644
--- 
a/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSum.java
+++ 
b/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSum.java
@@ -24,8 +24,10 @@ import java.util.zip.CRC32;
 import org.apache.commons.codec.binary.Hex;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
 import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.RowDataUtil;
+import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.Pipeline;
@@ -90,12 +92,7 @@ public class CheckSum extends BaseTransform<CheckSumMeta, 
CheckSumData> {
       data.suffix = Const.NVL(resolve(meta.getSuffix()), "");
 
       try {
-        if (meta.getCheckSumType() == CheckSumMeta.CheckSumType.MD5
-            || meta.getCheckSumType() == CheckSumMeta.CheckSumType.SHA1
-            || meta.getCheckSumType() == CheckSumMeta.CheckSumType.SHA256
-            || meta.getCheckSumType() == CheckSumMeta.CheckSumType.SHA384
-            || meta.getCheckSumType() == CheckSumMeta.CheckSumType.SHA512) {
-          // Case sensitive
+        if (meta.getCheckSumType() != null && 
meta.getCheckSumType().isDigest()) {
           data.digest = 
MessageDigest.getInstance(meta.getCheckSumType().getCode());
         }
       } catch (Exception e) {
@@ -106,8 +103,7 @@ public class CheckSum extends BaseTransform<CheckSumMeta, 
CheckSumData> {
     Object[] outputRowData = null;
 
     try {
-      if (meta.getCheckSumType() == CheckSumMeta.CheckSumType.ADLER32
-          || meta.getCheckSumType() == CheckSumMeta.CheckSumType.CRC32) {
+      if (meta.getCheckSumType() != null && 
meta.getCheckSumType().isIntegerResult()) {
         // get checksum
         Long checksum = calculCheckSum(r);
         outputRowData = RowDataUtil.addValueData(r, data.nrInfields, checksum);
@@ -236,6 +232,10 @@ public class CheckSum extends BaseTransform<CheckSumMeta, 
CheckSumData> {
   }
 
   private Long calculCheckSum(Object[] r) throws Exception {
+    if (meta.getCheckSumType() == CheckSumMeta.CheckSumType.HASHCODE) {
+      return calculateHashCode(r);
+    }
+
     Long retval;
     byte[] byteArray = buildCheckSumInputBytes(r);
 
@@ -257,6 +257,17 @@ public class CheckSum extends BaseTransform<CheckSumMeta, 
CheckSumData> {
     return retval;
   }
 
+  /** Native field values, including nulls. Prefix, separator and suffix are 
not included. */
+  private Long calculateHashCode(Object[] r) throws HopValueException {
+    RowMeta hashRowMeta = new RowMeta();
+    Object[] hashRow = new Object[data.fieldnr];
+    for (int i = 0; i < data.fieldnr; i++) {
+      
hashRowMeta.addValueMeta(getInputRowMeta().getValueMeta(data.fieldnrs[i]));
+      hashRow[i] = r[data.fieldnrs[i]];
+    }
+    return (long) hashRowMeta.hashCode(hashRow);
+  }
+
   @Override
   public boolean init() {
     if (super.init()) {
diff --git 
a/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSumDialog.java
 
b/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSumDialog.java
index bacd68043b..eb07743e78 100644
--- 
a/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSumDialog.java
+++ 
b/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSumDialog.java
@@ -73,6 +73,10 @@ public class CheckSumDialog extends BaseTransformDialog {
   private Label wlResultType;
   private CCombo wResultType;
 
+  private Label wlPrefix;
+  private Label wlSeparator;
+  private Label wlSuffix;
+
   public CheckSumDialog(
       Shell parent, IVariables variables, CheckSumMeta transformMeta, 
PipelineMeta pipelineMeta) {
     super(parent, variables, transformMeta, pipelineMeta);
@@ -103,6 +107,7 @@ public class CheckSumDialog extends BaseTransformDialog {
     fdType.top = new FormAttachment(wSpacer, margin);
     fdType.right = new FormAttachment(100, 0);
     wType.setLayoutData(fdType);
+    wType.setToolTipText(BaseMessages.getString(PKG, 
"CheckSumDialog.Type.ToolTip"));
     wType.addSelectionListener(
         new SelectionAdapter() {
           @Override
@@ -157,7 +162,7 @@ public class CheckSumDialog extends BaseTransformDialog {
     wResult.setLayoutData(fdResult);
 
     // Prefix
-    Label wlPrefix = new Label(shell, SWT.RIGHT);
+    wlPrefix = new Label(shell, SWT.RIGHT);
     wlPrefix.setText(BaseMessages.getString(PKG, 
"CheckSumDialog.Prefix.Label"));
     PropsUi.setLook(wlPrefix);
     FormData fdlPrefix = new FormData();
@@ -174,7 +179,7 @@ public class CheckSumDialog extends BaseTransformDialog {
     wPrefix.setLayoutData(fdPrefix);
 
     // Separator
-    Label wlSeparator = new Label(shell, SWT.RIGHT);
+    wlSeparator = new Label(shell, SWT.RIGHT);
     wlSeparator.setText(BaseMessages.getString(PKG, 
"CheckSumDialog.Separator.Label"));
     PropsUi.setLook(wlSeparator);
     FormData fdlSeparator = new FormData();
@@ -191,7 +196,7 @@ public class CheckSumDialog extends BaseTransformDialog {
     wSeparator.setLayoutData(fdSeparator);
 
     // Suffix
-    Label wlSuffix = new Label(shell, SWT.RIGHT);
+    wlSuffix = new Label(shell, SWT.RIGHT);
     wlSuffix.setText(BaseMessages.getString(PKG, 
"CheckSumDialog.Suffix.Label"));
     PropsUi.setLook(wlSuffix);
     FormData fdlSuffix = new FormData();
@@ -274,16 +279,19 @@ public class CheckSumDialog extends BaseTransformDialog {
   }
 
   private void activeResultType() {
-    int currentType = wType.getSelectionIndex();
-    // Only available for type MD5 and SHA
-    boolean active =
-        currentType == 2
-            || currentType == 3
-            || currentType == 4
-            || currentType == 5
-            || currentType == 6;
-    wlResultType.setEnabled(active);
-    wResultType.setEnabled(active);
+    CheckSumMeta.CheckSumType type =
+        CheckSumMeta.CheckSumType.getTypeFromDescription(wType.getText());
+    boolean digest = type.isDigest();
+    wlResultType.setEnabled(digest);
+    wResultType.setEnabled(digest);
+
+    boolean useFieldDecorations = type != CheckSumMeta.CheckSumType.HASHCODE;
+    wlPrefix.setEnabled(useFieldDecorations);
+    wPrefix.setEnabled(useFieldDecorations);
+    wlSeparator.setEnabled(useFieldDecorations);
+    wSeparator.setEnabled(useFieldDecorations);
+    wlSuffix.setEnabled(useFieldDecorations);
+    wSuffix.setEnabled(useFieldDecorations);
   }
 
   protected void setComboBoxes() {
diff --git 
a/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSumMeta.java
 
b/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSumMeta.java
index f62528bbb5..112316d2ce 100644
--- 
a/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSumMeta.java
+++ 
b/plugins/transforms/checksum/src/main/java/org/apache/hop/pipeline/transforms/checksum/CheckSumMeta.java
@@ -60,6 +60,7 @@ public class CheckSumMeta extends BaseTransformMeta<CheckSum, 
CheckSumData> {
     SHA256("SHA-256", BaseMessages.getString(PKG, "CheckSumMeta.Type.SHA256")),
     SHA384("SHA-384", BaseMessages.getString(PKG, "CheckSumMeta.Type.SHA384")),
     SHA512("SHA-512", BaseMessages.getString(PKG, "CheckSumMeta.Type.SHA512")),
+    HASHCODE("HASHCODE", BaseMessages.getString(PKG, 
"CheckSumMeta.Type.HASHCODE")),
     ;
 
     private String code;
@@ -105,6 +106,16 @@ public class CheckSumMeta extends 
BaseTransformMeta<CheckSum, CheckSumData> {
     public String getDescription() {
       return description;
     }
+
+    /** CRC32, ADLER32 and the Combination lookup/update hash are returned as 
an integer. */
+    public boolean isIntegerResult() {
+      return this == CRC32 || this == ADLER32 || this == HASHCODE;
+    }
+
+    /** Message digests can be returned as a string, hexadecimal or binary 
value. */
+    public boolean isDigest() {
+      return this == MD5 || this == SHA1 || this == SHA256 || this == SHA384 
|| this == SHA512;
+    }
   }
 
   public enum ResultType implements IEnumHasCode {
@@ -224,7 +235,7 @@ public class CheckSumMeta extends 
BaseTransformMeta<CheckSum, CheckSumData> {
     // Output field (String)
     if (!Utils.isEmpty(resultFieldName)) {
       IValueMeta v = null;
-      if (checkSumType == CheckSumType.CRC32 || checkSumType == 
CheckSumType.ADLER32) {
+      if (checkSumType != null && checkSumType.isIntegerResult()) {
         v = new ValueMetaInteger(variables.resolve(resultFieldName));
       } else {
         v =
diff --git 
a/plugins/transforms/checksum/src/main/resources/org/apache/hop/pipeline/transforms/checksum/messages/messages_en_US.properties
 
b/plugins/transforms/checksum/src/main/resources/org/apache/hop/pipeline/transforms/checksum/messages/messages_en_US.properties
index 1fb85c126c..9d85b50682 100644
--- 
a/plugins/transforms/checksum/src/main/resources/org/apache/hop/pipeline/transforms/checksum/messages/messages_en_US.properties
+++ 
b/plugins/transforms/checksum/src/main/resources/org/apache/hop/pipeline/transforms/checksum/messages/messages_en_US.properties
@@ -39,6 +39,7 @@ CheckSumDialog.ResultType.Label=Result type
 CheckSumDialog.Shell.Title=Add a checksum
 CheckSumDialog.TransformName.Label=Transform name
 CheckSumDialog.Type.Label=Type
+CheckSumDialog.Type.ToolTip=Hash code returns the same signed integer as 
Combination lookup/update. Prefix, separator and suffix are ignored for that 
type. Select the same fields, in the same order, as the key fields.
 CheckSumMeta.CheckResult.AllFieldsFound=All fields are found in the input 
stream.
 CheckSumMeta.CheckResult.FieldsFound=Fields that were not found in input 
stream:\n\n{0}
 CheckSumMeta.CheckResult.NoFieldsEntered=No fields are entered.
@@ -48,12 +49,13 @@ CheckSumMeta.CheckResult.ResultFieldMissing=Result 
fieldname is missing!
 CheckSumMeta.CheckResult.ResultFieldOK=Result fieldname is provided.
 CheckSumMeta.CheckResult.TransformRecevingData=Transform is connected to 
previous one, receiving {0} fields
 CheckSumMeta.CheckResult.TransformRecevingData2=Transform is receiving info 
from other transforms.
-CheckSumMeta.keyword=checksum,hash,md5,sha,crc32
+CheckSumMeta.keyword=checksum,hash,hashcode,md5,sha,crc32
 CheckSumMeta.ResultType.Binary=Binary
 CheckSumMeta.ResultType.Hexadecimal=Hexadecimal
 CheckSumMeta.ResultType.String=String
 CheckSumMeta.Type.ADLER32=ADLER 32
 CheckSumMeta.Type.CRC32=CRC 32
+CheckSumMeta.Type.HASHCODE=Hash code
 CheckSumMeta.Type.MD5=MD5
 CheckSumMeta.Type.SHA1=SHA-1
 CheckSumMeta.Type.SHA256=SHA-256
diff --git 
a/plugins/transforms/checksum/src/test/java/org/apache/hop/pipeline/transforms/checksum/CheckSumMetaTest.java
 
b/plugins/transforms/checksum/src/test/java/org/apache/hop/pipeline/transforms/checksum/CheckSumMetaTest.java
index 8669d6424e..9f483e076f 100644
--- 
a/plugins/transforms/checksum/src/test/java/org/apache/hop/pipeline/transforms/checksum/CheckSumMetaTest.java
+++ 
b/plugins/transforms/checksum/src/test/java/org/apache/hop/pipeline/transforms/checksum/CheckSumMetaTest.java
@@ -18,6 +18,8 @@
 package org.apache.hop.pipeline.transforms.checksum;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.Arrays;
 import java.util.HashMap;
@@ -25,6 +27,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.variables.Variables;
 import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
 import org.apache.hop.pipeline.transforms.loadsave.LoadSaveTester;
 import org.apache.hop.pipeline.transforms.loadsave.initializer.IInitializer;
@@ -53,6 +58,27 @@ class CheckSumMetaTest implements IInitializer<CheckSumMeta> 
{
     assertEquals("SHA-256", CheckSumMeta.CheckSumType.SHA256.getCode());
     assertEquals("SHA-384", CheckSumMeta.CheckSumType.SHA384.getCode());
     assertEquals("SHA-512", CheckSumMeta.CheckSumType.SHA512.getCode());
+    assertEquals("HASHCODE", CheckSumMeta.CheckSumType.HASHCODE.getCode());
+    assertTrue(CheckSumMeta.CheckSumType.HASHCODE.isIntegerResult());
+    assertTrue(CheckSumMeta.CheckSumType.CRC32.isIntegerResult());
+    assertTrue(CheckSumMeta.CheckSumType.ADLER32.isIntegerResult());
+    assertFalse(CheckSumMeta.CheckSumType.HASHCODE.isDigest());
+    assertTrue(CheckSumMeta.CheckSumType.MD5.isDigest());
+    assertFalse(CheckSumMeta.CheckSumType.MD5.isIntegerResult());
+  }
+
+  @Test
+  void hashCodeResultFieldIsAnInteger() throws HopException {
+    CheckSumMeta meta = new CheckSumMeta();
+    meta.setCheckSumType(CheckSumMeta.CheckSumType.HASHCODE);
+    meta.setResultFieldName("row_hash");
+    RowMeta rowMeta = new RowMeta();
+
+    meta.getFields(rowMeta, "checksum", null, null, new Variables(), null);
+
+    assertEquals(1, rowMeta.size());
+    assertEquals("row_hash", rowMeta.getValueMeta(0).getName());
+    assertEquals(IValueMeta.TYPE_INTEGER, rowMeta.getValueMeta(0).getType());
   }
 
   @Test
diff --git 
a/plugins/transforms/checksum/src/test/java/org/apache/hop/pipeline/transforms/checksum/CheckSumTest.java
 
b/plugins/transforms/checksum/src/test/java/org/apache/hop/pipeline/transforms/checksum/CheckSumTest.java
index 436f3ca8c2..b373c41201 100644
--- 
a/plugins/transforms/checksum/src/test/java/org/apache/hop/pipeline/transforms/checksum/CheckSumTest.java
+++ 
b/plugins/transforms/checksum/src/test/java/org/apache/hop/pipeline/transforms/checksum/CheckSumTest.java
@@ -36,6 +36,7 @@ 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.ValueMetaBinary;
+import org.apache.hop.core.row.value.ValueMetaInteger;
 import org.apache.hop.core.row.value.ValueMetaNumber;
 import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
@@ -378,6 +379,33 @@ class CheckSumTest {
         results.getWritten().get(0)[1]);
   }
 
+  @Test
+  void testHashCodeMatchesRowMetaHash() throws Exception {
+    RowMeta inputRowMeta = new RowMeta();
+    inputRowMeta.addValueMeta(new ValueMetaString("name"));
+    inputRowMeta.addValueMeta(new ValueMetaInteger("id"));
+    inputRowMeta.addValueMeta(new ValueMetaNumber("amount"));
+    inputRowMeta.addValueMeta(new ValueMetaBinary("payload"));
+    Object[] input = new Object[] {"abc", 5L, 10.8d, new byte[] {1, 2, 3}};
+
+    CheckSumMeta checkSumMeta = new CheckSumMeta();
+    checkSumMeta.setPrefix("CUST");
+    checkSumMeta.setSeparator("|");
+    checkSumMeta.setSuffix("END");
+    checkSumMeta.setFields(
+        List.of(new Field("name"), new Field("id"), new Field("amount"), new 
Field("payload")));
+
+    MockRowListener results =
+        executeHexTest(CheckSumMeta.CheckSumType.HASHCODE, input, 
inputRowMeta, checkSumMeta);
+    assertEquals(1, results.getWritten().size());
+    assertEquals((long) inputRowMeta.hashCode(input), 
results.getWritten().get(0)[4]);
+
+    Object[] nulls = new Object[] {null, null, null, null};
+    results = executeHexTest(CheckSumMeta.CheckSumType.HASHCODE, nulls, 
inputRowMeta, checkSumMeta);
+    assertEquals(1, results.getWritten().size());
+    assertEquals((long) inputRowMeta.hashCode(nulls), 
results.getWritten().get(0)[4]);
+  }
+
   @Test
   void testHexOutput_adler32() throws Exception {
     MockRowListener results =
diff --git 
a/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookup.java
 
b/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookup.java
index 339fa49ff0..eafbf4ee13 100644
--- 
a/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookup.java
+++ 
b/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookup.java
@@ -222,7 +222,7 @@ public class CombinationLookup extends 
BaseTransform<CombinationLookupMeta, Comb
       }
 
       if (meta.isUseHash()) {
-        valHash = (long) data.hashRowMeta.hashCode(hashRow);
+        valHash = hashValue(rowMeta, row, hashRow);
         lookupRow[lookupIndex] = valHash;
         lookupIndex++;
       }
@@ -329,6 +329,26 @@ public class CombinationLookup extends 
BaseTransform<CombinationLookupMeta, Comb
     return outputRow;
   }
 
+  Long hashValue(IRowMeta rowMeta, Object[] row, Object[] hashRow) throws 
HopValueException {
+    if (data.hashFieldNr >= 0) {
+      return rowMeta.getInteger(row, data.hashFieldNr);
+    }
+    return (long) data.hashRowMeta.hashCode(hashRow);
+  }
+
+  void resolveHashField(IRowMeta inputRowMeta) throws HopTransformException {
+    data.hashFieldNr = -1;
+    if (!meta.isUseHash() || 
Utils.isEmpty(resolve(meta.getHashFieldInStream()))) {
+      return;
+    }
+    String hashFieldName = resolve(meta.getHashFieldInStream());
+    data.hashFieldNr = inputRowMeta.indexOfValue(hashFieldName);
+    if (data.hashFieldNr < 0) {
+      throw new HopTransformException(
+          BaseMessages.getString(PKG, 
"CombinationLookup.Exception.FieldNotFound", hashFieldName));
+    }
+  }
+
   @Override
   public boolean processRow() throws HopException {
     Object[] r = getRow(); // Get row from input rowset & set row busy!
@@ -389,6 +409,8 @@ public class CombinationLookup extends 
BaseTransform<CombinationLookupMeta, Comb
         
data.hashRowMeta.addValueMeta(getInputRowMeta().getValueMeta(data.keynrs[i])); 
// KEYi = ?
       }
 
+      resolveHashField(getInputRowMeta());
+
       setCombiLookup(getInputRowMeta());
       preloadCache(data.hashRowMeta);
     }
diff --git 
a/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupData.java
 
b/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupData.java
index 5e36a41afa..1c3e1a2313 100644
--- 
a/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupData.java
+++ 
b/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupData.java
@@ -29,6 +29,7 @@ import org.apache.hop.pipeline.transform.ITransformData;
 public class CombinationLookupData extends BaseTransformData implements 
ITransformData {
   public Database db;
   public int[] keynrs; // nrs in row of the keys
+  public int hashFieldNr = -1;
 
   public Map<RowMetaAndData, Long> cache;
 
diff --git 
a/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupDialog.java
 
b/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupDialog.java
index 8124d0a998..2b3c800d74 100644
--- 
a/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupDialog.java
+++ 
b/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupDialog.java
@@ -41,6 +41,7 @@ import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.ComboVar;
 import org.apache.hop.ui.core.widget.MetaSelectionLine;
 import org.apache.hop.ui.core.widget.NamingSchemeTypes;
 import org.apache.hop.ui.core.widget.TableView;
@@ -106,6 +107,9 @@ public class CombinationLookupDialog extends 
BaseTransformDialog {
   private Label wlHashfield;
   private TextVar wHashfield;
 
+  private Label wlHashFieldInStream;
+  private ComboVar wHashFieldInStream;
+
   private Text wLastUpdateField;
 
   private ColumnInfo[] ciKey;
@@ -392,6 +396,28 @@ public class CombinationLookupDialog extends 
BaseTransformDialog {
     fdHashfield.right = new FormAttachment(100, 0);
     wHashfield.setLayoutData(fdHashfield);
 
+    wlHashFieldInStream = new Label(wGeneralComp, SWT.RIGHT);
+    wlHashFieldInStream.setText(
+        BaseMessages.getString(PKG, 
"CombinationLookupDialog.HashFieldInStream.Label"));
+    wlHashFieldInStream.setToolTipText(
+        BaseMessages.getString(PKG, 
"CombinationLookupDialog.HashFieldInStream.ToolTip"));
+    PropsUi.setLook(wlHashFieldInStream);
+    FormData fdlHashFieldInStream = new FormData();
+    fdlHashFieldInStream.left = new FormAttachment(0, 0);
+    fdlHashFieldInStream.right = new FormAttachment(middle, -margin);
+    fdlHashFieldInStream.top = new FormAttachment(wHashfield, margin);
+    wlHashFieldInStream.setLayoutData(fdlHashFieldInStream);
+    wHashFieldInStream = new ComboVar(variables, wGeneralComp, SWT.SINGLE | 
SWT.LEFT | SWT.BORDER);
+    wHashFieldInStream.setToolTipText(
+        BaseMessages.getString(PKG, 
"CombinationLookupDialog.HashFieldInStream.ToolTip"));
+    PropsUi.setLook(wHashFieldInStream);
+    wHashFieldInStream.addModifyListener(lsMod);
+    FormData fdHashFieldInStream = new FormData();
+    fdHashFieldInStream.left = new FormAttachment(middle, 0);
+    fdHashFieldInStream.top = new FormAttachment(wHashfield, margin);
+    fdHashFieldInStream.right = new FormAttachment(100, 0);
+    wHashFieldInStream.setLayoutData(fdHashFieldInStream);
+
     // Last update field:
     Label wlLastUpdateField = new Label(wGeneralComp, SWT.RIGHT);
     wlLastUpdateField.setText(
@@ -400,14 +426,14 @@ public class CombinationLookupDialog extends 
BaseTransformDialog {
     FormData fdlLastUpdateField = new FormData();
     fdlLastUpdateField.left = new FormAttachment(0, 0);
     fdlLastUpdateField.right = new FormAttachment(middle, -margin);
-    fdlLastUpdateField.top = new FormAttachment(wHashfield, margin);
+    fdlLastUpdateField.top = new FormAttachment(wHashFieldInStream, margin);
     wlLastUpdateField.setLayoutData(fdlLastUpdateField);
     wLastUpdateField = new Text(wGeneralComp, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
     PropsUi.setLook(wLastUpdateField);
     wLastUpdateField.addModifyListener(lsMod);
     FormData fdLastUpdateField = new FormData();
     fdLastUpdateField.left = new FormAttachment(middle, 0);
-    fdLastUpdateField.top = new FormAttachment(wHashfield, margin);
+    fdLastUpdateField.top = new FormAttachment(wHashFieldInStream, margin);
     fdLastUpdateField.right = new FormAttachment(100, 0);
     wLastUpdateField.setLayoutData(fdLastUpdateField);
 
@@ -633,11 +659,17 @@ public class CombinationLookupDialog extends 
BaseTransformDialog {
     //
     String[] fieldNames = ConstUi.sortFieldNames(inputFields);
     ciKey[1].setComboValues(fieldNames);
+    if (wHashFieldInStream != null && !wHashFieldInStream.isDisposed()) {
+      wHashFieldInStream.setItems(fieldNames);
+    }
   }
 
   public void enableFields() {
-    wHashfield.setEnabled(wHashcode.getSelection());
-    wlHashfield.setEnabled(wHashcode.getSelection());
+    boolean useHash = wHashcode.getSelection();
+    wHashfield.setEnabled(useHash);
+    wlHashfield.setEnabled(useHash);
+    wHashFieldInStream.setEnabled(useHash);
+    wlHashFieldInStream.setEnabled(useHash);
   }
 
   private void setTableFieldCombo() {
@@ -734,8 +766,6 @@ public class CombinationLookupDialog extends 
BaseTransformDialog {
     wPreloadCache.setSelection(input.isPreloadCache());
     wReplace.setSelection(input.isReplaceFields());
     wHashcode.setSelection(input.isUseHash());
-    wHashfield.setEnabled(input.isUseHash());
-    wlHashfield.setEnabled(input.isUseHash());
 
     String techKeyCreation = returnFields.getTechKeyCreation();
     if (techKeyCreation == null) {
@@ -785,6 +815,8 @@ public class CombinationLookupDialog extends 
BaseTransformDialog {
       wConnection.setText(input.getConnectionName());
     }
     wHashfield.setText(Const.NVL(input.getHashField(), ""));
+    wHashFieldInStream.setText(Const.NVL(input.getHashFieldInStream(), ""));
+    enableFields();
 
     wCommit.setText("" + input.getCommitSize());
     wCachesize.setText("" + input.getCacheSize());
@@ -834,6 +866,8 @@ public class CombinationLookupDialog extends 
BaseTransformDialog {
     in.setReplaceFields(wReplace.getSelection());
     in.setUseHash(wHashcode.getSelection());
     in.setHashField(wHashfield.getText());
+    String hashFieldInStream = wHashFieldInStream.getText();
+    in.setHashFieldInStream(Utils.isEmpty(hashFieldInStream) ? null : 
hashFieldInStream);
     in.setSchemaName(wSchema.getText());
     in.setTableName(wTable.getText());
     returnFields.setTechnicalKeyField(wTk.getText());
diff --git 
a/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupMeta.java
 
b/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupMeta.java
index 4f9b7f2fee..bcddb7b2d8 100644
--- 
a/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupMeta.java
+++ 
b/plugins/transforms/combinationlookup/src/main/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupMeta.java
@@ -117,6 +117,15 @@ public class CombinationLookupMeta
       injectionKeyDescription = "CombinationLookup.Injection.HASH_FIELD")
   private String hashField;
 
+  /**
+   * Optional input field that already contains the hash. Empty means this 
transform calculates it.
+   */
+  @HopMetadataProperty(
+      key = "hashfield_stream",
+      injectionKey = "HASH_FIELD_IN_STREAM",
+      injectionKeyDescription = 
"CombinationLookup.Injection.HASH_FIELD_IN_STREAM")
+  private String hashFieldInStream;
+
   /** Commit size for insert / update */
   @HopMetadataProperty(
       key = "commit",
@@ -325,6 +334,7 @@ public class CombinationLookupMeta
                     transformMeta);
           }
           remarks.add(cr);
+          checkHashField(remarks, variables, transformMeta, prev);
         } else {
           errorMessage =
               BaseMessages.getString(PKG, 
"CombinationLookupMeta.CheckResult.CouldNotReadFields")
@@ -413,6 +423,42 @@ public class CombinationLookupMeta
     }
   }
 
+  void checkHashField(
+      List<ICheckResult> remarks,
+      IVariables variables,
+      TransformMeta transformMeta,
+      IRowMeta prev) {
+    if (!useHash || Utils.isEmpty(variables.resolve(hashFieldInStream))) {
+      return;
+    }
+    String hashName = variables.resolve(hashFieldInStream);
+    IValueMeta hashValue = prev.searchValueMeta(hashName);
+    CheckResult cr;
+    if (hashValue == null) {
+      cr =
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_ERROR,
+              BaseMessages.getString(
+                  PKG, "CombinationLookupMeta.CheckResult.HashFieldNotFound", 
hashName),
+              transformMeta);
+    } else if (hashValue.getType() != IValueMeta.TYPE_INTEGER) {
+      cr =
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_ERROR,
+              BaseMessages.getString(
+                  PKG, 
"CombinationLookupMeta.CheckResult.HashFieldNotInteger", hashName),
+              transformMeta);
+    } else {
+      cr =
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_OK,
+              BaseMessages.getString(
+                  PKG, "CombinationLookupMeta.CheckResult.HashFieldFound", 
hashName),
+              transformMeta);
+    }
+    remarks.add(cr);
+  }
+
   @Override
   public SqlStatement getSqlStatements(
       IVariables variables,
diff --git 
a/plugins/transforms/combinationlookup/src/main/resources/org/apache/hop/pipeline/transforms/combinationlookup/messages/messages_en_US.properties
 
b/plugins/transforms/combinationlookup/src/main/resources/org/apache/hop/pipeline/transforms/combinationlookup/messages/messages_en_US.properties
index 0985af1e7b..5966b3bf72 100644
--- 
a/plugins/transforms/combinationlookup/src/main/resources/org/apache/hop/pipeline/transforms/combinationlookup/messages/messages_en_US.properties
+++ 
b/plugins/transforms/combinationlookup/src/main/resources/org/apache/hop/pipeline/transforms/combinationlookup/messages/messages_en_US.properties
@@ -23,6 +23,7 @@ CombinationLookup.Injection.CACHE_SIZE=The size of the cache.
 CombinationLookup.Injection.COMMIT_SIZE=The number of rows to commit at a time.
 CombinationLookup.Injection.CONNECTION_NAME=The name of the database 
connection.
 CombinationLookup.Injection.HASH_FIELD=The name of the field containing 
hashcodes.
+CombinationLookup.Injection.HASH_FIELD_IN_STREAM=The optional input field that 
already contains the hash code. When empty, the hash is calculated from the key 
fields.
 CombinationLookup.Injection.KEY_FIELD=The table field to use to lookup rows.
 CombinationLookup.Injection.KEY_FIELDS=The key fields to use to lookup rows.
 CombinationLookup.Injection.KEY_LOOKUP=The stream field to use to lookup rows.
@@ -60,6 +61,8 @@ CombinationLookupDialog.GetFields.Button=\ &Get Fields
 CombinationLookupDialog.GetSchemas.Error=ERROR
 CombinationLookupDialog.Hashcode.Label=Use hashcode
 CombinationLookupDialog.Hashfield.Label=Hashcode field in table
+CombinationLookupDialog.HashFieldInStream.Label=Hash field in stream
+CombinationLookupDialog.HashFieldInStream.ToolTip=Optional Integer field 
calculated earlier, for example by Add a checksum. When set, this value is 
stored in the hashcode field and used for lookup. Leave empty to calculate the 
hash from the key fields. Use checksum type Hash code on the same key fields, 
in the same order, to match that calculation.
 CombinationLookupDialog.KeyFieldsTab.TabTitle=Key fields
 CombinationLookupDialog.Keyfields.Label=Key fields (to look up row in table)\:
 CombinationLookupDialog.LastUpdateField.Label=Date of last update field 
(optional)
@@ -99,6 +102,9 @@ CombinationLookupMeta.CheckResult.ErrorNoSequenceName=No 
sequence name was speci
 CombinationLookupMeta.CheckResult.ErrorOccurred=An error occurred\: 
 CombinationLookupMeta.CheckResult.ErrorReadingSequence=Error reading sequence 
 CombinationLookupMeta.CheckResult.ErrorTechKeyCreation=Technical key creation 
method invalid
+CombinationLookupMeta.CheckResult.HashFieldFound=Hash field [{0}] was found in 
the input stream.
+CombinationLookupMeta.CheckResult.HashFieldNotFound=Hash field [{0}] was not 
found in the input stream.
+CombinationLookupMeta.CheckResult.HashFieldNotInteger=Hash field [{0}] must be 
an Integer.
 CombinationLookupMeta.CheckResult.InvalidConnection=Please select or create a 
connection\!
 CombinationLookupMeta.CheckResult.MissingCompareFields=Missing compare fields 
in target table\:
 CombinationLookupMeta.CheckResult.MissingFields=Missing fields, not found in 
input from previous transforms\:
diff --git 
a/plugins/transforms/combinationlookup/src/test/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupHashTest.java
 
b/plugins/transforms/combinationlookup/src/test/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupHashTest.java
new file mode 100644
index 0000000000..0806a1d187
--- /dev/null
+++ 
b/plugins/transforms/combinationlookup/src/test/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupHashTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.pipeline.transforms.combinationlookup;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class CombinationLookupHashTest {
+
+  @BeforeAll
+  static void setUp() throws Exception {
+    HopEnvironment.init();
+    PluginRegistry.init();
+  }
+
+  @Test
+  void calculatedHashMatchesTheRowHash() throws Exception {
+    CombinationLookup transform = transform();
+    RowMeta hashRowMeta = new RowMeta();
+    hashRowMeta.addValueMeta(new ValueMetaString("key"));
+    Object[] hashRow = new Object[] {"abc"};
+    transform.getData().hashRowMeta = hashRowMeta;
+    transform.getData().hashFieldNr = -1;
+
+    RowMeta rowMeta = rowMeta();
+    long expected = hashRowMeta.hashCode(hashRow);
+
+    assertEquals(expected, transform.hashValue(rowMeta, new Object[] {"abc", 
42L}, hashRow));
+    assertNotEquals(42L, expected);
+  }
+
+  @Test
+  void streamFieldReplacesTheCalculatedHash() throws Exception {
+    CombinationLookup transform = transform();
+    transform.getMeta().setUseHash(true);
+    transform.getMeta().setHashFieldInStream("${HASH_FIELD}");
+    transform.setVariable("HASH_FIELD", "hash");
+
+    RowMeta rowMeta = rowMeta();
+    transform.resolveHashField(rowMeta);
+    assertEquals(1, transform.getData().hashFieldNr);
+
+    RowMeta hashRowMeta = new RowMeta();
+    hashRowMeta.addValueMeta(new ValueMetaString("key"));
+    Object[] hashRow = new Object[] {"abc"};
+    transform.getData().hashRowMeta = hashRowMeta;
+
+    assertEquals(42L, transform.hashValue(rowMeta, new Object[] {"abc", 42L}, 
hashRow));
+    assertEquals(-15L, transform.hashValue(rowMeta, new Object[] {"abc", 
-15L}, hashRow));
+    assertNull(transform.hashValue(rowMeta, new Object[] {"abc", null}, 
hashRow));
+  }
+
+  @Test
+  void emptyStreamFieldKeepsTheCalculatedHash() throws Exception {
+    CombinationLookup transform = transform();
+    transform.getMeta().setUseHash(true);
+    transform.getMeta().setHashFieldInStream("");
+    transform.resolveHashField(rowMeta());
+    assertEquals(-1, transform.getData().hashFieldNr);
+
+    transform.getMeta().setUseHash(false);
+    transform.getMeta().setHashFieldInStream("hash");
+    transform.resolveHashField(rowMeta());
+    assertEquals(-1, transform.getData().hashFieldNr);
+  }
+
+  @Test
+  void missingStreamFieldFails() {
+    CombinationLookup transform = transform();
+    transform.getMeta().setUseHash(true);
+    transform.getMeta().setHashFieldInStream("missing");
+
+    HopTransformException exception =
+        assertThrows(HopTransformException.class, () -> 
transform.resolveHashField(rowMeta()));
+    assertTrue(exception.getMessage().contains("missing"));
+  }
+
+  @Test
+  void checkAcceptsAnIntegerStreamField() {
+    CombinationLookupMeta meta = new CombinationLookupMeta();
+    meta.setUseHash(true);
+    meta.setHashFieldInStream("${HASH_FIELD}");
+    Variables variables = new Variables();
+    variables.setVariable("HASH_FIELD", "row_hash");
+    RowMeta prev = new RowMeta();
+    prev.addValueMeta(new ValueMetaInteger("row_hash"));
+    List<ICheckResult> remarks = new ArrayList<>();
+
+    meta.checkHashField(remarks, variables, new TransformMeta("lookup", meta), 
prev);
+
+    assertEquals(1, remarks.size());
+    assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.get(0).getType());
+  }
+
+  @Test
+  void checkRejectsAMissingOrNonIntegerStreamField() {
+    CombinationLookupMeta meta = new CombinationLookupMeta();
+    meta.setUseHash(true);
+    meta.setHashFieldInStream("row_hash");
+    RowMeta prev = new RowMeta();
+    prev.addValueMeta(new ValueMetaString("row_hash"));
+    List<ICheckResult> remarks = new ArrayList<>();
+    TransformMeta transformMeta = new TransformMeta("lookup", meta);
+
+    meta.checkHashField(remarks, new Variables(), transformMeta, prev);
+    assertEquals(ICheckResult.TYPE_RESULT_ERROR, remarks.get(0).getType());
+
+    remarks.clear();
+    prev.clear();
+    meta.checkHashField(remarks, new Variables(), transformMeta, prev);
+    assertEquals(ICheckResult.TYPE_RESULT_ERROR, remarks.get(0).getType());
+
+    remarks.clear();
+    meta.setUseHash(false);
+    meta.checkHashField(remarks, new Variables(), transformMeta, prev);
+    assertTrue(remarks.isEmpty());
+  }
+
+  private static CombinationLookup transform() {
+    CombinationLookupMeta meta = new CombinationLookupMeta();
+    CombinationLookupData data = new CombinationLookupData();
+    TransformMeta transformMeta = new TransformMeta("lookup", meta);
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    pipelineMeta.addTransform(transformMeta);
+    return new CombinationLookup(transformMeta, meta, data, 0, pipelineMeta, 
null);
+  }
+
+  private static RowMeta rowMeta() {
+    RowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("key"));
+    rowMeta.addValueMeta(new ValueMetaInteger("hash"));
+    return rowMeta;
+  }
+}
diff --git 
a/plugins/transforms/combinationlookup/src/test/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupMetaTest.java
 
b/plugins/transforms/combinationlookup/src/test/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupMetaTest.java
index 6cf97afd9f..c4a6065921 100644
--- 
a/plugins/transforms/combinationlookup/src/test/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupMetaTest.java
+++ 
b/plugins/transforms/combinationlookup/src/test/java/org/apache/hop/pipeline/transforms/combinationlookup/CombinationLookupMetaTest.java
@@ -31,14 +31,18 @@ import org.apache.hop.core.plugins.PluginRegistry;
 import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.metadata.serializer.xml.XmlMetadataUtil;
 import org.apache.hop.pipeline.DatabaseImpact;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.transform.TransformMeta;
 import org.apache.hop.pipeline.transform.TransformSerializationTestUtil;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
 
 class CombinationLookupMetaTest {
   private static final Class<?> PKG = CombinationLookupMeta.class;
@@ -53,8 +57,28 @@ class CombinationLookupMetaTest {
 
   @Test
   void testSerialization() throws Exception {
-    TransformSerializationTestUtil.testSerialization(
-        "/combination-lookup-transform.xml", CombinationLookupMeta.class);
+    CombinationLookupMeta meta =
+        TransformSerializationTestUtil.testSerialization(
+            "/combination-lookup-transform.xml", CombinationLookupMeta.class);
+    assertNull(meta.getHashFieldInStream());
+  }
+
+  @Test
+  void hashFieldInStreamRoundTrips() throws Exception {
+    CombinationLookupMeta meta = new CombinationLookupMeta();
+    meta.setHashFieldInStream("row_hash");
+    String xml =
+        XmlHandler.openTag(TransformMeta.XML_TAG)
+            + meta.getXml()
+            + XmlHandler.closeTag(TransformMeta.XML_TAG);
+
+    Document document = XmlHandler.loadXmlString(xml);
+    Node node = XmlHandler.getSubNode(document, TransformMeta.XML_TAG);
+    CombinationLookupMeta copy =
+        XmlMetadataUtil.deSerializeFromXml(
+            node, CombinationLookupMeta.class, new MemoryMetadataProvider());
+
+    assertEquals("row_hash", copy.getHashFieldInStream());
   }
 
   /**

Reply via email to