You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/04/09 12:58:43 UTC

(hop) branch main updated: Replace if instanceof chains with modern switch pattern matching and add unit test (#6977)

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 ce77dbf1a1 Replace if instanceof chains with modern switch pattern matching and add unit test (#6977)
ce77dbf1a1 is described below

commit ce77dbf1a10782bd7716fbb49b16532463c99960
Author: Lance <le...@gmail.com>
AuthorDate: Thu Apr 9 20:58:38 2026 +0800

    Replace if instanceof chains with modern switch pattern matching and add unit test (#6977)
    
    * Replace if instanceof chains with modern switch pattern matching and add unit test
    
    Signed-off-by: lance <le...@gmail.com>
    
    * Replace if instanceof chains with modern switch pattern matching and add unit test
    
    Signed-off-by: lance <le...@gmail.com>
    
    ---------
    
    Signed-off-by: lance <le...@gmail.com>
---
 .../main/java/org/apache/hop/core/row/RowMeta.java |  61 ++++---
 .../org/apache/hop/core/row/ValueMetaAndData.java  |  65 ++-----
 .../hop/core/row/value/ValueMetaFactory.java       |  36 ++--
 .../java/org/apache/hop/core/row/RowMetaTests.java | 112 ++++++++++++
 .../apache/hop/core/row/ValueMetaAndDataTests.java | 106 +++++++++++
 .../hop/core/row/value/ValueMetaFactoryTests.java  | 120 +++++++++++++
 .../hop/avro/transforms/avrodecode/AvroDecode.java | 102 ++++-------
 .../transforms/avrodecode/AvroDecodeTests.java     | 197 +++++++++++++++++++++
 .../hop/pipeline/transforms/sasinput/SasInput.java |  72 ++++----
 .../transforms/sasinput/SasInputTests.java         | 154 ++++++++++++++++
 10 files changed, 822 insertions(+), 203 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/row/RowMeta.java b/core/src/main/java/org/apache/hop/core/row/RowMeta.java
index 1e138918f6..4b3deeb5d3 100644
--- a/core/src/main/java/org/apache/hop/core/row/RowMeta.java
+++ b/core/src/main/java/org/apache/hop/core/row/RowMeta.java
@@ -462,43 +462,48 @@ public class RowMeta implements IRowMeta {
    * (IRowMeta / IValueMeta) is used. Strings use getBytes().length; other types use fixed
    * estimates. Use this when you have only the raw row (e.g. from getRowFrom) and no row meta.
    *
-   * @param dataRow the row (may be null)
+   * @param dataRow the row (maybe null)
    * @return estimated size in bytes, or null if row is null (no data)
    */
   public static Long getRowSizeEstimateFromRow(Object[] dataRow) {
     if (dataRow == null) {
       return null;
     }
+
     long total = 0L;
     for (Object v : dataRow) {
-      if (v == null) {
-        continue;
-      }
-      if (v instanceof String s) {
-        total += s.getBytes().length;
-      } else if (v instanceof byte[] b) {
-        total += b.length;
-      } else if (v instanceof BigDecimal) {
-        total += 32;
-      } else if (v instanceof Number) {
-        total += 8;
-      } else if (v instanceof Date) {
-        total += 8;
-      } else if (v instanceof Boolean) {
-        total += 1;
-      } else if (v instanceof UUID) {
-        total += 36;
-      } else if (v instanceof JsonNode jn) {
-        total += jn.toString().length() * 2L;
-      } else if (v instanceof GenericRecord gr) {
-        total += gr.toString().length() * 2L;
-      } else if (v instanceof InetAddress ia) {
-        total += ia.getHostAddress().length() * 2L;
-      } else {
-        total += 64; // Serializable or other unknown types
-      }
+      total += estimateSize(v);
+    }
+    return total;
+  }
+
+  /**
+   * Estimates the size in bytes of a row from the Java types of its values only
+   *
+   * @param v data
+   * @return estimated size in bytes
+   */
+  private static long estimateSize(Object v) {
+    if (v == null) {
+      return 0;
     }
-    return Long.valueOf(total);
+
+    return switch (v) {
+      case String s -> s.getBytes().length;
+      case byte[] b -> b.length;
+      case BigDecimal ignored -> 32;
+      case Number ignored -> 8;
+
+      case Date ignored -> 8;
+      case Boolean ignored -> 1;
+      case UUID ignored -> 36;
+
+      case JsonNode jn -> jn.toString().length() * 2L;
+      case GenericRecord gr -> gr.toString().length() * 2L;
+      case InetAddress ia -> ia.getHostAddress().length() * 2L;
+
+      default -> 64;
+    };
   }
 
   /**
diff --git a/core/src/main/java/org/apache/hop/core/row/ValueMetaAndData.java b/core/src/main/java/org/apache/hop/core/row/ValueMetaAndData.java
index ca90362e61..fc00d40bfa 100644
--- a/core/src/main/java/org/apache/hop/core/row/ValueMetaAndData.java
+++ b/core/src/main/java/org/apache/hop/core/row/ValueMetaAndData.java
@@ -19,6 +19,8 @@ package org.apache.hop.core.row;
 
 import java.math.BigDecimal;
 import java.util.Date;
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopRuntimeException;
 import org.apache.hop.core.exception.HopValueException;
@@ -35,6 +37,8 @@ import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.core.xml.XmlHandler;
 import org.w3c.dom.Node;
 
+@Getter
+@Setter
 public class ValueMetaAndData {
   public static final String XML_TAG = "value";
 
@@ -44,8 +48,8 @@ public class ValueMetaAndData {
   public ValueMetaAndData() {}
 
   /**
-   * @param valueMeta
-   * @param valueData
+   * @param valueMeta IValueMeta
+   * @param valueData Object
    */
   public ValueMetaAndData(IValueMeta valueMeta, Object valueData) {
     this.valueMeta = valueMeta;
@@ -54,23 +58,18 @@ public class ValueMetaAndData {
 
   public ValueMetaAndData(String valueName, Object valueData) {
     this.valueData = valueData;
-    if (valueData instanceof String) {
-      this.valueMeta = new ValueMetaString(valueName);
-    } else if (valueData instanceof Double) {
-      this.valueMeta = new ValueMetaNumber(valueName);
-    } else if (valueData instanceof Long) {
-      this.valueMeta = new ValueMetaInteger(valueName);
-    } else if (valueData instanceof Date) {
-      this.valueMeta = new ValueMetaDate(valueName);
-    } else if (valueData instanceof BigDecimal) {
-      this.valueMeta = new ValueMetaBigNumber(valueName);
-    } else if (valueData instanceof Boolean) {
-      this.valueMeta = new ValueMetaBoolean(valueName);
-    } else if (valueData instanceof byte[]) {
-      this.valueMeta = new ValueMetaBinary(valueName);
-    } else {
-      this.valueMeta = new ValueMetaSerializable(valueName);
-    }
+
+    this.valueMeta =
+        switch (valueData) {
+          case String ignored -> new ValueMetaString(valueName);
+          case Double ignored -> new ValueMetaNumber(valueName);
+          case Long ignored -> new ValueMetaInteger(valueName);
+          case Date ignored -> new ValueMetaDate(valueName);
+          case BigDecimal ignored -> new ValueMetaBigNumber(valueName);
+          case Boolean ignored -> new ValueMetaBoolean(valueName);
+          case byte[] ignored -> new ValueMetaBinary(valueName);
+          default -> new ValueMetaSerializable(valueName);
+        };
   }
 
   public ValueMetaAndData(ValueMetaAndData v) {
@@ -203,32 +202,4 @@ public class ValueMetaAndData {
   public String toStringMeta() {
     return valueMeta.toStringMeta();
   }
-
-  /**
-   * @return the valueData
-   */
-  public Object getValueData() {
-    return valueData;
-  }
-
-  /**
-   * @param valueData the valueData to set
-   */
-  public void setValueData(Object valueData) {
-    this.valueData = valueData;
-  }
-
-  /**
-   * @return the valueMeta
-   */
-  public IValueMeta getValueMeta() {
-    return valueMeta;
-  }
-
-  /**
-   * @param valueMeta the valueMeta to set
-   */
-  public void setValueMeta(IValueMeta valueMeta) {
-    this.valueMeta = valueMeta;
-  }
 }
diff --git a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaFactory.java b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaFactory.java
index 7f92fb7472..bf4e55e077 100644
--- a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaFactory.java
+++ b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaFactory.java
@@ -192,33 +192,21 @@ public class ValueMetaFactory {
    * will be found usable this may be implemented later.
    *
    * @param object object to guess applicable IValueMeta.
-   * @return
+   * @return IValueMeta
    * @see IValueMeta if the hop value meta is recognized, null otherwise.
    */
   public static IValueMeta guessValueMetaInterface(Object object) {
-    if (object instanceof Number) {
-      // this is numeric object
-      if (object instanceof BigDecimal) {
-        return new ValueMetaBigNumber();
-      } else if (object instanceof Double) {
-        return new ValueMetaNumber();
-      } else if (object instanceof Long) {
-        return new ValueMetaInteger();
-      }
-    } else if (object instanceof String) {
-      return new ValueMetaString();
-    } else if (object instanceof Date) {
-      return new ValueMetaDate();
-    } else if (object instanceof Boolean) {
-      return new ValueMetaBoolean();
-    } else if (object instanceof byte[]) {
-      return new ValueMetaBinary();
-    } else if (object instanceof JsonNode) {
-      return new ValueMetaJson();
-    }
-
-    // ask someone else
-    return null;
+    return switch (object) {
+      case BigDecimal ignored -> new ValueMetaBigNumber();
+      case Double ignored -> new ValueMetaNumber();
+      case Long ignored -> new ValueMetaInteger();
+      case String ignored -> new ValueMetaString();
+      case Date ignored -> new ValueMetaDate();
+      case Boolean ignored -> new ValueMetaBoolean();
+      case byte[] ignored -> new ValueMetaBinary();
+      case JsonNode ignored -> new ValueMetaJson();
+      case null, default -> null;
+    };
   }
 
   public static IValueMeta loadValueMetaFromJson(JSONObject jValue) throws HopPluginException {
diff --git a/core/src/test/java/org/apache/hop/core/row/RowMetaTests.java b/core/src/test/java/org/apache/hop/core/row/RowMetaTests.java
new file mode 100644
index 0000000000..4406de6291
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/row/RowMetaTests.java
@@ -0,0 +1,112 @@
+/*
+ * 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.core.row;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.math.BigDecimal;
+import java.net.InetAddress;
+import java.util.Date;
+import java.util.UUID;
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.hop.junit.rules.RestoreHopEnvironmentExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/** Unit test for {@link RowMeta} */
+@ExtendWith(RestoreHopEnvironmentExtension.class)
+class RowMetaTests {
+  private static final ObjectMapper MAPPER = new ObjectMapper();
+
+  @Test
+  void testNullRow() {
+    assertNull(RowMeta.getRowSizeEstimateFromRow(null));
+  }
+
+  @Test
+  void testAllTypesCoverage() throws Exception {
+    // String,3 bytes (UTF-8)
+    String str = "abc";
+    // byte[]
+    byte[] bytes = new byte[] {1, 2, 3, 4};
+    // BigDecimal
+    BigDecimal bd = new BigDecimal("123.45");
+    // Number
+    int num = 100;
+    // Date
+    Date date = new Date();
+    // Boolean
+    boolean bool = true;
+    // UUID
+    UUID uuid = UUID.randomUUID();
+    // JsonNode
+    JsonNode jsonNode = MAPPER.readTree("{\"a\":1}");
+    // GenericRecord
+    Schema schema =
+        new Schema.Parser()
+            .parse(
+                "{\"type\":\"record\",\"name\":\"Test\",\"fields\":[{\"name\":\"f\",\"type\":\"string\"}]}");
+    GenericRecord rec = new GenericData.Record(schema);
+    rec.put("f", "v");
+    // InetAddress
+    InetAddress ip = InetAddress.getByName("127.0.0.1");
+    // Unknown type
+    Object unknown = new Object();
+
+    Object[] row =
+        new Object[] {null, str, bytes, bd, num, date, bool, uuid, jsonNode, rec, ip, unknown};
+
+    Long result = RowMeta.getRowSizeEstimateFromRow(row);
+    long expected = 0;
+    // String
+    expected += str.getBytes().length;
+    // byte[]
+    expected += bytes.length;
+    // BigDecimal
+    expected += 32;
+    // Number
+    expected += 8;
+    // Date
+    expected += 8;
+    // Boolean
+    expected += 1;
+    // UUID
+    expected += 36;
+    // JsonNode
+    expected += jsonNode.toString().length() * 2L;
+    // GenericRecord
+    expected += rec.toString().length() * 2L;
+    // InetAddress
+    expected += ip.getHostAddress().length() * 2L;
+    // default
+    expected += 64;
+    assertEquals(expected, result);
+  }
+
+  @Test
+  void testOnlyNullElements() {
+    Object[] row = new Object[] {null, null, null};
+    Long result = RowMeta.getRowSizeEstimateFromRow(row);
+    assertEquals(0L, result);
+  }
+}
diff --git a/core/src/test/java/org/apache/hop/core/row/ValueMetaAndDataTests.java b/core/src/test/java/org/apache/hop/core/row/ValueMetaAndDataTests.java
new file mode 100644
index 0000000000..ffa4d3e68b
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/row/ValueMetaAndDataTests.java
@@ -0,0 +1,106 @@
+/*
+ * 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.core.row;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import org.apache.hop.core.row.value.ValueMetaBigNumber;
+import org.apache.hop.core.row.value.ValueMetaBinary;
+import org.apache.hop.core.row.value.ValueMetaBoolean;
+import org.apache.hop.core.row.value.ValueMetaDate;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaSerializable;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link ValueMetaAndData} */
+class ValueMetaAndDataTests {
+
+  @Test
+  void testStringType() {
+    ValueMetaAndData vm = new ValueMetaAndData("f1", "hello");
+
+    assertInstanceOf(ValueMetaString.class, vm.getValueMeta());
+    assertEquals("hello", vm.getValueData());
+  }
+
+  @Test
+  void testDoubleType() {
+    ValueMetaAndData vm = new ValueMetaAndData("f2", 1.23d);
+
+    assertInstanceOf(ValueMetaNumber.class, vm.getValueMeta());
+    assertEquals(1.23d, vm.getValueData());
+  }
+
+  @Test
+  void testLongType() {
+    ValueMetaAndData vm = new ValueMetaAndData("f3", 123L);
+
+    assertInstanceOf(ValueMetaInteger.class, vm.getValueMeta());
+    assertEquals(123L, vm.getValueData());
+  }
+
+  @Test
+  void testDateType() {
+    Date now = new Date();
+    ValueMetaAndData vm = new ValueMetaAndData("f4", now);
+
+    assertInstanceOf(ValueMetaDate.class, vm.getValueMeta());
+    assertEquals(now, vm.getValueData());
+  }
+
+  @Test
+  void testBigDecimalType() {
+    BigDecimal bd = new BigDecimal("123.45");
+    ValueMetaAndData vm = new ValueMetaAndData("f5", bd);
+
+    assertInstanceOf(ValueMetaBigNumber.class, vm.getValueMeta());
+    assertEquals(bd, vm.getValueData());
+  }
+
+  @Test
+  void testBooleanType() {
+    ValueMetaAndData vm = new ValueMetaAndData("f6", true);
+
+    assertInstanceOf(ValueMetaBoolean.class, vm.getValueMeta());
+    assertEquals(true, vm.getValueData());
+  }
+
+  @Test
+  void testByteArrayType() {
+    byte[] bytes = new byte[] {1, 2, 3};
+    ValueMetaAndData vm = new ValueMetaAndData("f7", bytes);
+
+    assertInstanceOf(ValueMetaBinary.class, vm.getValueMeta());
+    assertArrayEquals(bytes, (byte[]) vm.getValueData());
+  }
+
+  @Test
+  void testDefaultSerializableType() {
+    Object obj = new Object();
+    ValueMetaAndData vm = new ValueMetaAndData("f8", obj);
+
+    assertInstanceOf(ValueMetaSerializable.class, vm.getValueMeta());
+    assertEquals(obj, vm.getValueData());
+  }
+}
diff --git a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaFactoryTests.java b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaFactoryTests.java
new file mode 100644
index 0000000000..50af3cfbe7
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaFactoryTests.java
@@ -0,0 +1,120 @@
+/*
+ * 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.core.row.value;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hop.core.row.IValueMeta;
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link ValueMetaFactory} */
+class ValueMetaFactoryTests {
+
+  @Test
+  void testBigDecimal() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(new BigDecimal("10.5"));
+    assertInstanceOf(ValueMetaBigNumber.class, meta);
+  }
+
+  @Test
+  void testDouble() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(10.5d);
+    assertInstanceOf(ValueMetaNumber.class, meta);
+  }
+
+  @Test
+  void testLong() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(10L);
+    assertInstanceOf(ValueMetaInteger.class, meta);
+  }
+
+  @Test
+  void testInteger_shouldReturnNullOrNeedClarification() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(10);
+    assertNull(meta, "Integer is Number but not explicitly handled");
+  }
+
+  @Test
+  void testFloat() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(10.5f);
+    assertNull(meta);
+  }
+
+  @Test
+  void testString() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface("hello");
+    assertInstanceOf(ValueMetaString.class, meta);
+  }
+
+  @Test
+  void testDate() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(new Date());
+    assertInstanceOf(ValueMetaDate.class, meta);
+  }
+
+  @Test
+  void testBoolean_true() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(true);
+    assertInstanceOf(ValueMetaBoolean.class, meta);
+  }
+
+  @Test
+  void testBoolean_false() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(false);
+    assertInstanceOf(ValueMetaBoolean.class, meta);
+  }
+
+  @Test
+  void testByteArray() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface("abc".getBytes());
+    assertInstanceOf(ValueMetaBinary.class, meta);
+  }
+
+  @Test
+  void testJsonNode() throws Exception {
+    ObjectMapper mapper = new ObjectMapper();
+    JsonNode node = mapper.readTree("{\"a\":1}");
+
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(node);
+    assertInstanceOf(ValueMetaJson.class, meta);
+  }
+
+  @Test
+  void testNull() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(null);
+    assertNull(meta);
+  }
+
+  @Test
+  void testUnknownObject() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(new Object());
+    assertNull(meta);
+  }
+
+  @Test
+  void testAtomicInteger_edgeCase() {
+    IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(new AtomicInteger(5));
+    assertNull(meta);
+  }
+}
diff --git a/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrodecode/AvroDecode.java b/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrodecode/AvroDecode.java
index 0988984c34..9d15e22da9 100644
--- a/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrodecode/AvroDecode.java
+++ b/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrodecode/AvroDecode.java
@@ -17,10 +17,8 @@
 
 package org.apache.hop.avro.transforms.avrodecode;
 
-import java.util.Map;
+import com.google.common.annotations.VisibleForTesting;
 import org.apache.avro.Schema;
-import org.apache.avro.generic.GenericContainer;
-import org.apache.avro.generic.GenericData;
 import org.apache.avro.generic.GenericRecord;
 import org.apache.avro.util.Utf8;
 import org.apache.hop.core.exception.HopException;
@@ -107,7 +105,7 @@ public class AvroDecode extends BaseTransform<AvroDecodeMeta, AvroDecodeData> {
     return true;
   }
 
-  public static final int getStandardHopType(Schema.Field field) throws HopException {
+  public static int getStandardHopType(Schema.Field field) throws HopException {
     Schema.Type type = field.schema().getType();
     int basicType = getBasicType(type);
     if (basicType != 0) {
@@ -139,72 +137,38 @@ public class AvroDecode extends BaseTransform<AvroDecodeMeta, AvroDecodeData> {
     };
   }
 
-  public static final Object getStandardHopObject(Schema.Field field, Object avroValue)
-      throws HopException {
-    Object hopValue;
+  @VisibleForTesting
+  public static Object getStandardHopObject(Schema.Field field, Object avroValue) {
     if (avroValue == null) {
-      hopValue = null;
-    } else {
-      Schema.Type type = field.schema().getType();
-      switch (type) {
-        case NULL:
-          hopValue = null;
-          break;
-        case ENUM:
-          hopValue = avroValue.toString();
-          break;
-        case STRING:
-          hopValue = ((Utf8) avroValue).toString();
-          break;
-        case BYTES, LONG, DOUBLE:
-          hopValue = avroValue;
-          break;
-        case INT:
-          hopValue = (long) (int) avroValue;
-          break;
-        case FLOAT:
-          hopValue = (double) (float) avroValue;
-          break;
-        case BOOLEAN:
-          hopValue = (boolean) avroValue;
-          break;
-        case RECORD:
-          GenericData.Record record = (GenericData.Record) avroValue;
-          hopValue = record.toString();
-          break;
-        case ARRAY:
-          GenericData.Array array = (GenericData.Array) avroValue;
-          hopValue = array.toString();
-          break;
-        case MAP:
-          Map<Utf8, Object> map = (Map<Utf8, Object>) avroValue;
-          hopValue = map.toString();
-          break;
-        case UNION:
-          // This value can be a set of possible values...
-          //
-          if (avroValue instanceof Long
-              || avroValue instanceof Double
-              || avroValue instanceof String
-              || avroValue instanceof Boolean
-              || avroValue instanceof byte[]) {
-            hopValue = avroValue;
-          } else if (avroValue instanceof Float) {
-            hopValue = Double.valueOf((float) avroValue);
-          } else if (avroValue instanceof Integer) {
-            hopValue = Integer.valueOf((int) avroValue).longValue();
-          } else {
-            hopValue = avroValue.toString();
-          }
-          break;
-        case FIXED:
-          GenericContainer container = (GenericContainer) avroValue;
-          hopValue = container.toString();
-          break;
-        default:
-          throw new HopException("Schema type " + type + " isn't handled yet");
-      }
+      return null;
     }
-    return hopValue;
+
+    return convertBySchemaType(field.schema().getType(), avroValue);
+  }
+
+  private static Object convertBySchemaType(Schema.Type type, Object avroValue) {
+    return switch (type) {
+      case NULL -> null;
+      case ENUM, RECORD, ARRAY, MAP, FIXED -> avroValue.toString();
+      case STRING -> ((Utf8) avroValue).toString();
+      case BYTES, LONG, DOUBLE -> avroValue;
+      case INT -> (long) (int) avroValue;
+      case FLOAT -> (double) (float) avroValue;
+      case BOOLEAN -> (boolean) avroValue;
+      case UNION -> union(avroValue);
+    };
+  }
+
+  private static Object union(Object avroValue) {
+    return switch (avroValue) {
+      case Long l -> l;
+      case Double d -> d;
+      case String s -> s;
+      case Boolean b -> b;
+      case byte[] b -> b;
+      case Float f -> (double) f;
+      case Integer i -> i.longValue();
+      default -> avroValue.toString();
+    };
   }
 }
diff --git a/plugins/tech/avro/src/test/java/org/apache/hop/avro/transforms/avrodecode/AvroDecodeTests.java b/plugins/tech/avro/src/test/java/org/apache/hop/avro/transforms/avrodecode/AvroDecodeTests.java
new file mode 100644
index 0000000000..8ee5353f1f
--- /dev/null
+++ b/plugins/tech/avro/src/test/java/org/apache/hop/avro/transforms/avrodecode/AvroDecodeTests.java
@@ -0,0 +1,197 @@
+/*
+ * 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.avro.transforms.avrodecode;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.avro.Schema;
+import org.apache.avro.SchemaBuilder;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.util.Utf8;
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link AvroDecode} */
+class AvroDecodeTests {
+
+  @Test
+  void testNullValue() {
+    Schema schema = Schema.create(Schema.Type.STRING);
+    Object result = AvroDecode.getStandardHopObject(field("f", schema), null);
+    assertNull(result);
+  }
+
+  @Test
+  void testNullType() {
+    Schema schema = Schema.create(Schema.Type.NULL);
+    Object result = AvroDecode.getStandardHopObject(field("f", schema), "anything");
+    assertNull(result);
+  }
+
+  @Test
+  void testEnum() {
+    Schema schema = SchemaBuilder.enumeration("E").symbols("A", "B");
+    GenericData.EnumSymbol enumVal = new GenericData.EnumSymbol(schema, "A");
+
+    Object result = AvroDecode.getStandardHopObject(field("f", schema), enumVal);
+    assertEquals("A", result);
+  }
+
+  @Test
+  void testString() {
+    Schema schema = Schema.create(Schema.Type.STRING);
+    Utf8 utf8 = new Utf8("abc");
+
+    Object result = AvroDecode.getStandardHopObject(field("f", schema), utf8);
+    assertEquals("abc", result);
+  }
+
+  @Test
+  void testBytes() {
+    Schema schema = Schema.create(Schema.Type.BYTES);
+    byte[] data = new byte[] {1, 2, 3};
+
+    Object result = AvroDecode.getStandardHopObject(field("f", schema), data);
+    assertSame(data, result);
+  }
+
+  @Test
+  void testLongAndDouble() {
+    assertEquals(
+        123L, AvroDecode.getStandardHopObject(field("f", Schema.create(Schema.Type.LONG)), 123L));
+
+    assertEquals(
+        1.23, AvroDecode.getStandardHopObject(field("f", Schema.create(Schema.Type.DOUBLE)), 1.23));
+  }
+
+  @Test
+  void testInt() {
+    Object result = AvroDecode.getStandardHopObject(field("f", Schema.create(Schema.Type.INT)), 10);
+
+    assertEquals(10L, result);
+    assertInstanceOf(Long.class, result);
+  }
+
+  @Test
+  void testFloat() {
+    Object result =
+        AvroDecode.getStandardHopObject(field("f", Schema.create(Schema.Type.FLOAT)), 1.5f);
+
+    assertEquals(1.5d, result);
+    assertInstanceOf(Double.class, result);
+  }
+
+  @Test
+  void testBoolean() {
+    Object result =
+        AvroDecode.getStandardHopObject(field("f", Schema.create(Schema.Type.BOOLEAN)), true);
+    assertEquals(true, result);
+  }
+
+  @Test
+  void testRecord() {
+    Schema schema = SchemaBuilder.record("R").fields().requiredString("f").endRecord();
+    GenericRecord rec = new GenericData.Record(schema);
+    rec.put("f", "v");
+
+    Object result = AvroDecode.getStandardHopObject(field("f", schema), rec);
+    assertEquals(rec.toString(), result);
+  }
+
+  @Test
+  void testArray() {
+    Schema schema = Schema.createArray(Schema.create(Schema.Type.STRING));
+    GenericData.Array<String> array = new GenericData.Array<>(schema, List.of("a", "b"));
+
+    Object result = AvroDecode.getStandardHopObject(field("f", schema), array);
+    assertEquals(array.toString(), result);
+  }
+
+  @Test
+  void testMap() {
+    Schema schema = Schema.createMap(Schema.create(Schema.Type.STRING));
+    Map<Utf8, Object> map = new HashMap<>();
+    map.put(new Utf8("k"), "v");
+
+    Object result = AvroDecode.getStandardHopObject(field("f", schema), map);
+    assertEquals(map.toString(), result);
+  }
+
+  @Test
+  void testUnion() {
+    Schema unionSchema =
+        Schema.createUnion(
+            List.of(
+                Schema.create(Schema.Type.NULL),
+                Schema.create(Schema.Type.LONG),
+                Schema.create(Schema.Type.STRING)));
+
+    Schema.Field f = field("f", unionSchema);
+
+    // Long
+    assertEquals(10L, AvroDecode.getStandardHopObject(f, 10L));
+    // Double
+    assertEquals(1.2, AvroDecode.getStandardHopObject(f, 1.2));
+    // String
+    assertEquals("abc", AvroDecode.getStandardHopObject(f, "abc"));
+    // Boolean
+    assertEquals(true, AvroDecode.getStandardHopObject(f, true));
+    // byte[]
+    byte[] bytes = new byte[] {1};
+    assertSame(bytes, AvroDecode.getStandardHopObject(f, bytes));
+
+    // Float → Double
+    Object fResult = AvroDecode.getStandardHopObject(f, 1.5f);
+    assertEquals(1.5d, fResult);
+
+    // Integer → Long
+    Object iResult = AvroDecode.getStandardHopObject(f, 5);
+    assertEquals(5L, iResult);
+
+    // other → toString
+    Object other = new Object();
+    assertEquals(other.toString(), AvroDecode.getStandardHopObject(f, other));
+  }
+
+  @Test
+  void testFixed() {
+    Schema schema = SchemaBuilder.fixed("F").size(4);
+    GenericData.Fixed fixed = new GenericData.Fixed(schema, new byte[] {1, 2, 3, 4});
+
+    Object result = AvroDecode.getStandardHopObject(field("A", schema), fixed);
+    assertEquals(fixed.toString(), result);
+  }
+
+  @Test
+  void testDefaultException() {
+    assertThrows(
+        Exception.class,
+        () -> AvroDecode.getStandardHopObject(field("A", Schema.create(Schema.Type.ENUM)), null));
+  }
+
+  private Schema.Field field(String name, Schema schema) {
+    return new Schema.Field(name, schema, null, null);
+  }
+}
diff --git a/plugins/transforms/sasinput/src/main/java/org/apache/hop/pipeline/transforms/sasinput/SasInput.java b/plugins/transforms/sasinput/src/main/java/org/apache/hop/pipeline/transforms/sasinput/SasInput.java
index b5aad3d605..b7895785be 100644
--- a/plugins/transforms/sasinput/src/main/java/org/apache/hop/pipeline/transforms/sasinput/SasInput.java
+++ b/plugins/transforms/sasinput/src/main/java/org/apache/hop/pipeline/transforms/sasinput/SasInput.java
@@ -20,7 +20,10 @@ package org.apache.hop.pipeline.transforms.sasinput;
 import com.epam.parso.Column;
 import com.epam.parso.SasFileProperties;
 import com.epam.parso.impl.SasFileReaderImpl;
+import com.google.common.annotations.VisibleForTesting;
 import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -146,47 +149,20 @@ public class SasInput extends BaseTransform<SasInputMeta, SasInputData> {
             field.setPrecision(column.getFormat().getPrecision());
             field.setType(SasUtil.getHopDataType(column.getType()));
           }
+
           Object sasValue = sasRow[index];
-          Object value = null;
-          IValueMeta inputValueMeta = null;
           String fieldName = Const.NVL(field.getRename(), field.getName());
           int outputIndex = getInputRowMeta().size() + i;
-          if (sasValue instanceof byte[] bytes) {
-            inputValueMeta = new ValueMetaString(fieldName);
-            if (sasFileProperties.getEncoding() != null) {
-              value = new String(bytes, sasFileProperties.getEncoding());
-            } else {
-              value = new String(bytes);
-            }
-          }
-          if (sasValue instanceof String) {
-            inputValueMeta = new ValueMetaString(fieldName);
-            value = sasValue;
-          }
-          if (sasValue instanceof Double) {
-            inputValueMeta = new ValueMetaNumber(fieldName);
-            value = sasValue;
-          }
-          if (sasValue instanceof Float) {
-            inputValueMeta = new ValueMetaNumber(fieldName);
-            value = sasValue;
-          }
-          if (sasValue instanceof Long) {
-            inputValueMeta = new ValueMetaInteger(fieldName);
-            value = sasValue;
-          }
-          if (sasValue instanceof Integer) {
-            inputValueMeta = new ValueMetaInteger(fieldName);
-            value = (long) (int) sasValue;
-          }
-          if (sasValue instanceof Date) {
-            inputValueMeta = new ValueMetaDate(fieldName);
-            value = sasValue;
-          }
-          if (inputValueMeta != null) {
+
+          ConvertedValue converted = convertSasValue(sasValue, fieldName, sasFileProperties);
+          if (converted != null) {
+            IValueMeta inputValueMeta = converted.meta;
+            Object value = converted.value;
+
             inputValueMeta.setLength(field.getLength());
             inputValueMeta.setPrecision(field.getPrecision());
             inputValueMeta.setConversionMask(field.getConversionMask());
+
             IValueMeta outputValueMeta = data.outputRowMeta.getValueMeta(outputIndex);
             outputRow[outputIndex] = outputValueMeta.convertData(inputValueMeta, value);
           }
@@ -240,4 +216,30 @@ public class SasInput extends BaseTransform<SasInputMeta, SasInputData> {
     }
     return indexes;
   }
+
+  @VisibleForTesting
+  public ConvertedValue convertSasValue(Object object, String field, SasFileProperties property) {
+    return switch (object) {
+      case byte[] bytes -> {
+        Charset charset =
+            property.getEncoding() != null
+                ? Charset.forName(property.getEncoding())
+                : StandardCharsets.UTF_8;
+
+        String value = new String(bytes, charset);
+        yield new ConvertedValue(new ValueMetaString(field), value);
+      }
+
+      case String s -> new ConvertedValue(new ValueMetaString(field), s);
+      case Double d -> new ConvertedValue(new ValueMetaNumber(field), d);
+      case Float f -> new ConvertedValue(new ValueMetaNumber(field), f);
+      case Long l -> new ConvertedValue(new ValueMetaInteger(field), l);
+      case Integer i -> new ConvertedValue(new ValueMetaInteger(field), (long) i);
+      case Date date -> new ConvertedValue(new ValueMetaDate(field), date);
+      case null, default -> null;
+    };
+  }
+
+  /** class valueMeta, object */
+  public record ConvertedValue(IValueMeta meta, Object value) {}
 }
diff --git a/plugins/transforms/sasinput/src/test/java/org/apache/hop/pipeline/transforms/sasinput/SasInputTests.java b/plugins/transforms/sasinput/src/test/java/org/apache/hop/pipeline/transforms/sasinput/SasInputTests.java
new file mode 100644
index 0000000000..47b1ce44ae
--- /dev/null
+++ b/plugins/transforms/sasinput/src/test/java/org/apache/hop/pipeline/transforms/sasinput/SasInputTests.java
@@ -0,0 +1,154 @@
+/*
+ * 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.sasinput;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import com.epam.parso.SasFileProperties;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.row.value.ValueMetaDate;
+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.RestoreHopEnvironmentExtension;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+
+/** Unit test for {@link SasInput} */
+@ExtendWith(RestoreHopEnvironmentExtension.class)
+class SasInputTests {
+
+  private SasInput input;
+
+  @BeforeEach
+  void setup() {
+    TransformMockHelper<SasInputMeta, SasInputData> helper =
+        new TransformMockHelper<>("sas_input_test", SasInputMeta.class, SasInputData.class);
+    Mockito.doReturn(helper.iLogChannel)
+        .when(helper.logChannelFactory)
+        .create(any(), any(ILoggingObject.class));
+
+    when(helper.pipeline.isRunning()).thenReturn(true);
+
+    SasInputMeta meta = helper.iTransformMeta;
+    SasInputData data = helper.iTransformData;
+    PipelineMeta plMeta = helper.pipelineMeta;
+    Pipeline pipeline = helper.pipeline;
+    input = new SasInput(helper.transformMeta, meta, data, 0, plMeta, pipeline);
+  }
+
+  @Test
+  void testByteArray_withEncoding() {
+    byte[] bytes = "Bob".getBytes(StandardCharsets.UTF_8);
+    SasInput.ConvertedValue result = input.convertSasValue(bytes, "name", property("UTF-8"));
+
+    assertNotNull(result);
+    assertInstanceOf(ValueMetaString.class, result.meta());
+    assertEquals("Bob", result.value());
+  }
+
+  @Test
+  void testByteArray_defaultEncoding() {
+    byte[] bytes = "world".getBytes(StandardCharsets.UTF_8);
+
+    SasInput.ConvertedValue result = input.convertSasValue(bytes, "field1", property(null));
+    assertNotNull(result);
+    assertEquals("world", result.value());
+  }
+
+  @Test
+  void testString() {
+    SasInput.ConvertedValue result = input.convertSasValue("abc", "field1", property(null));
+
+    assertInstanceOf(ValueMetaString.class, result.meta());
+    assertEquals("abc", result.value());
+  }
+
+  @Test
+  void testDouble() {
+    SasInput.ConvertedValue result = input.convertSasValue(10.5d, "field1", property(null));
+
+    assertInstanceOf(ValueMetaNumber.class, result.meta());
+    assertEquals(10.5d, result.value());
+  }
+
+  @Test
+  void testFloat() {
+    SasInput.ConvertedValue result = input.convertSasValue(3.14f, "field1", property(null));
+
+    assertInstanceOf(ValueMetaNumber.class, result.meta());
+    assertEquals(3.14f, result.value());
+  }
+
+  @Test
+  void testLong() {
+    SasInput.ConvertedValue result = input.convertSasValue(100L, "field1", property(null));
+
+    assertInstanceOf(ValueMetaInteger.class, result.meta());
+    assertEquals(100L, result.value());
+  }
+
+  @Test
+  void testInteger() {
+    SasInput.ConvertedValue result = input.convertSasValue(42, "field1", property(null));
+
+    assertInstanceOf(ValueMetaInteger.class, result.meta());
+    assertEquals(42L, result.value());
+  }
+
+  @Test
+  void testDate() {
+    Date now = new Date();
+    SasInput.ConvertedValue result = input.convertSasValue(now, "field1", property(null));
+
+    assertInstanceOf(ValueMetaDate.class, result.meta());
+    assertEquals(now, result.value());
+  }
+
+  @Test
+  void testNull() {
+    SasInput.ConvertedValue result = input.convertSasValue(null, "field1", property(null));
+
+    assertNull(result);
+  }
+
+  @Test
+  void testDefaultUnknownType() {
+    SasInput.ConvertedValue result = input.convertSasValue(new Object(), "field1", property(null));
+
+    assertNull(result);
+  }
+
+  private SasFileProperties property(String encoding) {
+    SasFileProperties p = new SasFileProperties();
+    p.setEncoding(encoding);
+    return p;
+  }
+}