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 213f992093 align value mapper behaviour between new or existing field,
fixes #5586 (#7013)
213f992093 is described below
commit 213f9920939b6dcacf7c3a7d00c1971a8bae67bc
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Apr 16 20:12:14 2026 +0200
align value mapper behaviour between new or existing field, fixes #5586
(#7013)
---
.../pages/pipeline/transforms/valuemapper.adoc | 45 ++++--
.../transforms/valuemapper/ValueMapper.java | 21 ++-
.../transforms/valuemapper/ValueMapperDialog.java | 41 ++++-
.../transforms/valuemapper/ValueMapperMeta.java | 41 ++++-
.../valuemapper/messages/messages_en_US.properties | 3 +
.../valuemapper/ValueMapperMetaInjectionTest.java | 1 +
.../valuemapper/ValueMapperMetaTest.java | 36 ++++-
.../transforms/valuemapper/ValueMapperTest.java | 175 +++++++++++++++++++++
8 files changed, 339 insertions(+), 24 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc
index 02d1ddd284..d136a6ab22 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc
@@ -59,21 +59,44 @@ The following properties are used to define the mappings:
|===
|Option|Description
|Transform Name|Name of the transform this name has to be unique in a single
pipeline.
-|Fieldname to use|Field to use as the mapping source
-|Target field name|Field to use as the mapping Target
-|Default upon non-matching|Defines a default value for situations where the
source value is not empty, but there is no match
-|Field values table|Contains the mapping of source value to converted target
value.
-|Target field type |Specifies the data type of the mapped (target) values.
-If not set, the transform falls back to:
+|Fieldname to use|Field to use as the mapping source.
+|Target field name|Field to write mapped values to.
+Leave empty to overwrite the source field in place.
+|Keep original value on non-match|When enabled, a source value that does not
match any row in the field values table keeps the **original source value**
(when overwriting in place) or copies it into the **new target field**.
+In that case **Default upon non-matching** is disabled in the dialog and
ignored at runtime.
+
+When disabled, a non-match uses **Default upon non-matching** if it is set;
otherwise the output is **null** (for both overwrite and new-field modes).
+|Default upon non-matching|Defines a default value when the source value is
not empty but does not match any mapping row.
+Disabled when **Keep original value on non-match** is enabled.
+|Target field type|Specifies the data type of the mapped (target) values.
+If not set, the transform uses **String** (including when overwriting an
existing field in place, so the output column type matches string mapped
values).
+|Field values table|Table with one row per mapping: **Source value**, **Target
value**, and **Empty string equals null** (Yes/No per row).
+See <<empty-string-equals-null,Empty string equals null>> below.
-- String when writing to a new target field.
+|===
-- The same type as the source field when overwriting the source field.
-It fails if the values are incompatible.
+[[keep-original-on-non-match]]
+=== Keep original value on non-match
-|===
+Use this when unmatched values should stay as the incoming data instead of
becoming null or the default.
+
+* Overwrite mode (no target field): the source field is left unchanged for
non-matches (except normal storage-type handling where applicable).
+* New field mode: the new column receives the source value converted to the
target field type.
+
+When the option is on, **Default upon non-matching** is not used.
+
+[[empty-string-equals-null]]
+=== Empty string equals null (field values)
+
+Each mapping row has an **Empty string equals null** column (Yes/No).
+
+* **Yes** (default for new rows): after variables are resolved, if the
**target** value for that row is blank, the transform stores **null** as the
mapped result instead of converting an empty string to the target type.
+* **No**: an empty target is converted like any other value (for example an
empty string for a string-typed target).
+
+This applies both to normal source-to-target rows and to the special row used
to map null/empty source values when <<mapping-null-values,mapping null and
empty source values>> is configured.
+[[mapping-null-values]]
=== Mapping NULL values
If there is an empty source value defined, NULLs and empty strings are mapped
to the corresponding target value. Only one empty mapping is allowed.
-If the input stream contains NULL values, and no mapping for NULLs is defined,
NULL is returned (and not the default target)
+If the input stream contains NULL values, and no mapping for NULLs is defined,
NULL is returned (and not the default target).
diff --git
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
index fc63af53e4..f7195c6c2a 100644
---
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
+++
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
@@ -101,6 +101,7 @@ public class ValueMapper extends
BaseTransform<ValueMapperMeta, ValueMapperData>
Object sourceData =
data.sourceValueMeta.convertToNormalStorageType(sourceValue);
Object target = null;
boolean mapped = false;
+ boolean keepOriginalOnNonMatch = meta.isKeepOriginalValueOnNonMatch();
if (data.emptySourceMappingDefined && (r[data.keynr] == null || sourceData
== null)) {
target = data.emptyFieldValue;
@@ -116,21 +117,25 @@ public class ValueMapper extends
BaseTransform<ValueMapperMeta, ValueMapperData>
}
if (!Utils.isEmpty(meta.getTargetField())) {
- // room for the target
+ int lastIdx = data.outputMeta.size() - 1;
r = RowDataUtil.resizeArray(r, data.outputMeta.size());
- // Did we find anything to map to?
- r[data.outputMeta.size() - 1] = target;
+ if (mapped) {
+ r[lastIdx] = target;
+ } else if (keepOriginalOnNonMatch) {
+ r[lastIdx] = data.outputValueMeta.convertData(data.sourceValueMeta,
sourceValue);
+ } else {
+ r[lastIdx] = null;
+ }
} else {
if (mapped) {
r[data.keynr] = target;
- } else {
- // Convert to normal storage type.
- // Otherwise we're going to be mixing storage types.
- //
+ } else if (keepOriginalOnNonMatch) {
if (data.sourceValueMeta.isStorageBinaryString()) {
Object normal =
data.sourceValueMeta.convertToNormalStorageType(r[data.keynr]);
r[data.keynr] = normal;
}
+ } else {
+ r[data.keynr] = null;
}
}
putRow(data.outputMeta, r);
@@ -160,7 +165,7 @@ public class ValueMapper extends
BaseTransform<ValueMapperMeta, ValueMapperData>
// --- Default for non-match --------------
//
try {
- if (!Utils.isEmpty(meta.getNonMatchDefault())) {
+ if (!meta.isKeepOriginalValueOnNonMatch() &&
!Utils.isEmpty(meta.getNonMatchDefault())) {
nonMatchActivated = true;
String nonMatchStr = resolve(meta.getNonMatchDefault());
data.nonMatchDefault = keyFromString(data.targetValueMeta,
stringValueMeta, nonMatchStr);
diff --git
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
index e6462c834d..b4132beb12 100644
---
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
+++
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
@@ -36,9 +36,12 @@ import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TableItem;
@@ -53,6 +56,8 @@ public class ValueMapperDialog extends BaseTransformDialog {
private TextVar wNonMatchDefault;
+ private Label wlNonMatchDefault;
+
private TableView wFields;
private final ValueMapperMeta input;
@@ -61,6 +66,8 @@ public class ValueMapperDialog extends BaseTransformDialog {
private CCombo wTargetType;
+ private Button wKeepOriginalOnNonMatch;
+
public ValueMapperDialog(
Shell parent,
IVariables variables,
@@ -129,21 +136,40 @@ public class ValueMapperDialog extends
BaseTransformDialog {
fdTargetFieldname.right = new FormAttachment(100, 0);
wTargetFieldName.setLayoutData(fdTargetFieldname);
+ wKeepOriginalOnNonMatch = new Button(shell, SWT.CHECK);
+ wKeepOriginalOnNonMatch.setText(
+ BaseMessages.getString(PKG,
"ValueMapperDialog.KeepOriginalOnNonMatch.Label"));
+ wKeepOriginalOnNonMatch.setToolTipText(
+ BaseMessages.getString(PKG,
"ValueMapperDialog.KeepOriginalOnNonMatch.Tooltip"));
+ PropsUi.setLook(wKeepOriginalOnNonMatch);
+ FormData fdKeepOriginal = new FormData();
+ fdKeepOriginal.left = new FormAttachment(middle, 0);
+ fdKeepOriginal.top = new FormAttachment(wTargetFieldName, margin);
+ fdKeepOriginal.right = new FormAttachment(100, 0);
+ wKeepOriginalOnNonMatch.setLayoutData(fdKeepOriginal);
+ wKeepOriginalOnNonMatch.addSelectionListener(
+ new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateNonMatchDefaultEnabled();
+ }
+ });
+
// Non match default line
- Label wlNonMatchDefault = new Label(shell, SWT.RIGHT);
+ wlNonMatchDefault = new Label(shell, SWT.RIGHT);
wlNonMatchDefault.setText(
BaseMessages.getString(PKG,
"ValueMapperDialog.NonMatchDefault.Label"));
PropsUi.setLook(wlNonMatchDefault);
FormData fdlNonMatchDefault = new FormData();
fdlNonMatchDefault.left = new FormAttachment(0, 0);
fdlNonMatchDefault.right = new FormAttachment(middle, -margin);
- fdlNonMatchDefault.top = new FormAttachment(wTargetFieldName, margin);
+ fdlNonMatchDefault.top = new FormAttachment(wKeepOriginalOnNonMatch,
margin);
wlNonMatchDefault.setLayoutData(fdlNonMatchDefault);
wNonMatchDefault = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
PropsUi.setLook(wNonMatchDefault);
FormData fdNonMatchDefault = new FormData();
fdNonMatchDefault.left = new FormAttachment(middle, 0);
- fdNonMatchDefault.top = new FormAttachment(wTargetFieldName, margin);
+ fdNonMatchDefault.top = new FormAttachment(wKeepOriginalOnNonMatch,
margin);
fdNonMatchDefault.right = new FormAttachment(100, 0);
wNonMatchDefault.setLayoutData(fdNonMatchDefault);
@@ -260,6 +286,8 @@ public class ValueMapperDialog extends BaseTransformDialog {
if (input.getTargetType() != null) {
wTargetType.setText(input.getTargetType());
}
+
wKeepOriginalOnNonMatch.setSelection(input.isKeepOriginalValueOnNonMatch());
+ updateNonMatchDefaultEnabled();
int i = 0;
for (Values v : input.getValues()) {
@@ -296,6 +324,7 @@ public class ValueMapperDialog extends BaseTransformDialog {
input.setFieldToUse(wFieldName.getText());
input.setTargetField(wTargetFieldName.getText());
input.setNonMatchDefault(wNonMatchDefault.getText());
+
input.setKeepOriginalValueOnNonMatch(wKeepOriginalOnNonMatch.getSelection());
input.setTargetType(wTargetType.getText());
input.getValues().clear();
@@ -310,4 +339,10 @@ public class ValueMapperDialog extends BaseTransformDialog
{
dispose();
}
+
+ private void updateNonMatchDefaultEnabled() {
+ boolean keep = wKeepOriginalOnNonMatch.getSelection();
+ wNonMatchDefault.setEnabled(!keep);
+ wlNonMatchDefault.setEnabled(!keep);
+ }
}
diff --git
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
index 9294d03699..2ac0420377 100644
---
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
+++
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
@@ -19,9 +19,11 @@ package org.apache.hop.pipeline.transforms.valuemapper;
import java.util.ArrayList;
import java.util.List;
+import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.apache.hop.core.CheckResult;
+import org.apache.hop.core.Const;
import org.apache.hop.core.ICheckResult;
import org.apache.hop.core.annotations.Transform;
import org.apache.hop.core.exception.HopPluginException;
@@ -38,7 +40,7 @@ import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.BaseTransformMeta;
import org.apache.hop.pipeline.transform.TransformMeta;
-/** Maps String values of a certain field to new values */
+/** Maps String values of a certain field to new values. */
@Transform(
id = "ValueMapper",
image = "valuemapper.svg",
@@ -70,6 +72,42 @@ public class ValueMapperMeta extends
BaseTransformMeta<ValueMapper, ValueMapperD
injectionKeyDescription = "ValueMapper.Injection.NON_MATCH_DEFAULT")
private String nonMatchDefault;
+ /** Stored as Y/N when set; {@code null} when absent from serialized
metadata. */
+ @HopMetadataProperty(
+ key = "keep_original_value_on_non_match",
+ injectionKey = "KEEP_ORIGINAL_ON_NON_MATCH",
+ injectionKeyDescription =
"ValueMapper.Injection.KEEP_ORIGINAL_ON_NON_MATCH")
+ @Getter(AccessLevel.NONE)
+ @Setter(AccessLevel.NONE)
+ private String keepOriginalValueOnNonMatch;
+
+ /**
+ * When true, non-matching values keep the source field value (in-place or
copied into the new
+ * target field). The non-match default is ignored. When false, a non-match
uses the default if
+ * set, otherwise null (same for overwrite and new-field modes).
+ */
+ public boolean isKeepOriginalValueOnNonMatch() {
+ if (!Utils.isEmpty(keepOriginalValueOnNonMatch)) {
+ return Const.toBoolean(keepOriginalValueOnNonMatch);
+ }
+ return Utils.isEmpty(getTargetField()) &&
Utils.isEmpty(getNonMatchDefault());
+ }
+
+ /** Persists an explicit Y/N; used by the dialog and metadata injection. */
+ public void setKeepOriginalValueOnNonMatch(boolean value) {
+ this.keepOriginalValueOnNonMatch = value ? "Y" : "N";
+ }
+
+ public String getKeepOriginalValueOnNonMatch() {
+ return keepOriginalValueOnNonMatch;
+ }
+
+ /** Raw Y/N from metadata; empty clears to unset. */
+ public void setKeepOriginalValueOnNonMatch(String
keepOriginalValueOnNonMatch) {
+ this.keepOriginalValueOnNonMatch =
+ Utils.isEmpty(keepOriginalValueOnNonMatch) ? null :
keepOriginalValueOnNonMatch;
+ }
+
@HopMetadataProperty(
key = "target_type",
injectionKey = "TARGET_TYPE",
@@ -97,6 +135,7 @@ public class ValueMapperMeta extends
BaseTransformMeta<ValueMapper, ValueMapperD
this.fieldToUse = meta.fieldToUse;
this.targetField = meta.targetField;
this.nonMatchDefault = meta.nonMatchDefault;
+ this.keepOriginalValueOnNonMatch = meta.getKeepOriginalValueOnNonMatch();
if (meta.targetType != null && meta.targetType.isEmpty()) {
this.targetType = meta.targetType;
} else {
diff --git
a/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
b/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
index ff486b5882..abd98d1054 100644
---
a/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
+++
b/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
@@ -18,6 +18,7 @@
ValueMapper.Description=Value mapper
ValueMapper.Injection.FIELDNAME=The source field to use for mapping
ValueMapper.Injection.NON_MATCH_DEFAULT=The default value if there is not a
match during lookup
+ValueMapper.Injection.KEEP_ORIGINAL_ON_NON_MATCH=When Y, keep the source field
value on non-match (default-on-non-match is ignored)
ValueMapper.Injection.SOURCE=The source values for mapping
ValueMapper.Injection.TARGET=The target values for mapping
ValueMapper.Injection.EMPTY_STRING_EQUALS_NULL=When Y, an empty target value
is emitted as null
@@ -35,6 +36,8 @@ ValueMapperDialog.Fields.Column.TargetValue=Target value
ValueMapperDialog.Fields.Column.EmptyStringEqualsNull=Empty string equals null
ValueMapperDialog.Fields.Label=Field values\:
ValueMapperDialog.NonMatchDefault.Label=Default upon non-matching
+ValueMapperDialog.KeepOriginalOnNonMatch.Label=Keep original value on non-match
+ValueMapperDialog.KeepOriginalOnNonMatch.Tooltip=When enabled, non-matching
values keep the source value and the default-on-non-match value is not used.
ValueMapperDialog.TargetFieldname.Label=Target field name (empty\=overwrite)
ValueMapperDialog.TransformName.Label=Transform name
ValueMapperMeta.CheckResult.NotReceivingFieldsFromPreviousTransforms=Not
receiving any fields from previous transforms\!
diff --git
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
index 429aef1dc6..b11311dbb9 100644
---
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
+++
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
@@ -39,6 +39,7 @@ class ValueMapperMetaInjectionTest extends
BaseMetadataInjectionTestJunit5<Value
check("FIELDNAME", () -> meta.getFieldToUse());
check("TARGET_FIELDNAME", () -> meta.getTargetField());
check("NON_MATCH_DEFAULT", () -> meta.getNonMatchDefault());
+ check("KEEP_ORIGINAL_ON_NON_MATCH", () ->
meta.isKeepOriginalValueOnNonMatch());
check("SOURCE", () -> meta.getValues().get(0).getSource());
check("TARGET", () -> meta.getValues().get(0).getTarget());
check("EMPTY_STRING_EQUALS_NULL", () ->
meta.getValues().get(0).isEmptyStringEqualsNull());
diff --git
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
index f91e1d6f73..40562ee6cc 100644
---
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
+++
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
@@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import java.util.ArrayList;
@@ -89,7 +90,7 @@ class ValueMapperMetaTest {
assertEquals(
true,
v.isEmptyStringEqualsNull(),
- "Older pipelines without empty_string_equals_null must default to Y
(true)");
+ "Missing empty_string_equals_null defaults empty string equals null
to true");
}
}
@@ -111,6 +112,10 @@ class ValueMapperMetaTest {
assertEquals("Country_Name", meta.getTargetField());
assertEquals("[${NOT_FOUND}]", meta.getNonMatchDefault());
+ assertNull(meta.getKeepOriginalValueOnNonMatch());
+ assertFalse(
+ meta.isKeepOriginalValueOnNonMatch(),
+ "Unset keep-original resolves to N when a target field or non-match
default exists");
assertNull(meta.getTargetType(), "Expected null target type right after
deserialization");
// Before getFields: input has only the source field
IRowMeta input = new RowMeta();
@@ -129,6 +134,32 @@ class ValueMapperMetaTest {
"New target field should default to String type when no explicit
target type is set");
}
+ @Test
+ void implicitKeepOriginalTrueWhenUnsetInPlaceAndNoDefault() {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setTargetField("");
+ meta.setNonMatchDefault(null);
+ assertNull(meta.getKeepOriginalValueOnNonMatch());
+ assertTrue(meta.isKeepOriginalValueOnNonMatch());
+ }
+
+ @Test
+ void implicitKeepOriginalFalseWhenUnsetWithTargetField() {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setTargetField("out");
+ assertNull(meta.getKeepOriginalValueOnNonMatch());
+ assertFalse(meta.isKeepOriginalValueOnNonMatch());
+ }
+
+ @Test
+ void implicitKeepOriginalFalseWhenUnsetWithNonMatchDefaultOnly() {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setTargetField("");
+ meta.setNonMatchDefault("fallback");
+ assertNull(meta.getKeepOriginalValueOnNonMatch());
+ assertFalse(meta.isKeepOriginalValueOnNonMatch());
+ }
+
@Test
void cloneCopiesValuesAndEmptyStringEqualsNull() {
ValueMapperMeta meta = new ValueMapperMeta();
@@ -139,8 +170,11 @@ class ValueMapperMetaTest {
v.setEmptyStringEqualsNull(false);
meta.getValues().add(v);
+ meta.setKeepOriginalValueOnNonMatch(true);
ValueMapperMeta clone = (ValueMapperMeta) meta.clone();
assertEquals(1, clone.getValues().size());
+ assertTrue(clone.isKeepOriginalValueOnNonMatch());
+ assertEquals("Y", clone.getKeepOriginalValueOnNonMatch());
assertFalse(clone.getValues().get(0).isEmptyStringEqualsNull());
assertEquals("x", clone.getValues().get(0).getSource());
assertEquals("y", clone.getValues().get(0).getTarget());
diff --git
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperTest.java
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperTest.java
index 44e6cc6592..3ad23f0fc5 100644
---
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperTest.java
+++
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperTest.java
@@ -220,6 +220,181 @@ class ValueMapperTest {
assertNull(outputs.get(0)[0]);
}
+ @Test
+ void inPlaceNoMatchUnsetCompatKeepsSourceWhenNoDefault() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType(null);
+ meta.getValues().add(mapping("A", "B"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"Z"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals("Z", outputs.get(0)[0]);
+ }
+
+ @Test
+ void inPlaceNoMatchExplicitNoEmitsNullWhenNoDefault() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType(null);
+ meta.setKeepOriginalValueOnNonMatch(false);
+ meta.getValues().add(mapping("A", "B"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"Z"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertNull(outputs.get(0)[0]);
+ }
+
+ @Test
+ void newFieldNoMatchWithoutDefaultEmitsNull() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("mapped");
+ meta.setTargetType(null);
+ meta.getValues().add(mapping("A", "B"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"Z"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals("Z", outputs.get(0)[0]);
+ assertNull(outputs.get(0)[1]);
+ }
+
+ @Test
+ void keepOriginalOnNonMatchInPlacePreservesSource() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType(null);
+ meta.setKeepOriginalValueOnNonMatch(true);
+ meta.getValues().add(mapping("A", "B"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"Z"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals("Z", outputs.get(0)[0]);
+ }
+
+ @Test
+ void keepOriginalOnNonMatchNewFieldCopiesSource() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("mapped");
+ meta.setTargetType(null);
+ meta.setKeepOriginalValueOnNonMatch(true);
+ meta.getValues().add(mapping("A", "B"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"Z"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals("Z", outputs.get(0)[0]);
+ assertEquals("Z", outputs.get(0)[1]);
+ }
+
+ @Test
+ void keepOriginalIgnoresNonMatchDefaultAtRuntime() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType(null);
+ meta.setKeepOriginalValueOnNonMatch(true);
+ meta.setNonMatchDefault("SHOULD_NOT_USE");
+ meta.getValues().add(mapping("A", "B"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"Z"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals("Z", outputs.get(0)[0]);
+ }
+
@Test
void nonMatchDefaultUsedWhenNoMapping() throws Exception {
ValueMapperMeta meta = new ValueMapperMeta();