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

mattcasters 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 f6c0adc068 Fix timestamp-to-date conversion (#7820)
f6c0adc068 is described below

commit f6c0adc0687cec149d8c292c5f1ba644bc2d7d7a
Author: Gabriel Dutra <[email protected]>
AuthorDate: Fri Oct 9 06:18:44 2026 -0700

    Fix timestamp-to-date conversion (#7820)
    
    * Fix timestamp-to-date conversion
    
    * Expand timestamp conversion coverage
    
    * Issue #5651 : Normalize Date results in convertDataCompatible
    
    convertData already copied a Timestamp or java.sql.Date to a plain
    java.util.Date when the destination type is Date. convertDataCompatible,
    used when Stream Lookup coerces a key, still returned the source value.
    Both paths now share that copy. Timestamp fields still return the
    original Timestamp, nanoseconds included.
    
    ---------
    
    Co-authored-by: mattcasters <[email protected]>
---
 .../apache/hop/core/row/value/ValueMetaBase.java   |  16 ++-
 .../hop/core/row/value/ValueMetaTimestampTest.java | 110 ++++++++++++++++++++-
 2 files changed, 123 insertions(+), 3 deletions(-)

diff --git 
a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java 
b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
index 4f88008dc0..eac1795b7d 100644
--- a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
+++ b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
@@ -4501,7 +4501,7 @@ public class ValueMetaBase implements IValueMeta {
       case TYPE_NONE, TYPE_STRING -> meta2.getString(data2);
       case TYPE_NUMBER -> meta2.getNumber(data2);
       case TYPE_INTEGER -> meta2.getInteger(data2);
-      case TYPE_DATE -> meta2.getDate(data2);
+      case TYPE_DATE -> toDate(meta2.getDate(data2));
       case TYPE_BIGNUMBER -> meta2.getBigNumber(data2);
       case TYPE_BOOLEAN -> meta2.getBoolean(data2);
       case TYPE_BINARY -> meta2.getBinary(data2);
@@ -4525,7 +4525,7 @@ public class ValueMetaBase implements IValueMeta {
       case TYPE_STRING -> meta2.getCompatibleString(data2);
       case TYPE_NUMBER -> meta2.getNumber(data2);
       case TYPE_INTEGER -> meta2.getInteger(data2);
-      case TYPE_DATE -> meta2.getDate(data2);
+      case TYPE_DATE -> toDate(meta2.getDate(data2));
       case TYPE_BIGNUMBER -> meta2.getBigNumber(data2);
       case TYPE_BOOLEAN -> meta2.getBoolean(data2);
       case TYPE_BINARY -> meta2.getBinary(data2);
@@ -4534,6 +4534,18 @@ public class ValueMetaBase implements IValueMeta {
     };
   }
 
+  /**
+   * A conversion to Date must not keep a {@link java.sql.Timestamp} or {@link 
java.sql.Date}. Both
+   * extend {@link Date}, and JDBC treats those subclasses differently. An 
instance that is already
+   * a plain {@code Date} is returned as-is. Timestamp fields do not use this 
method.
+   */
+  private static Date toDate(Date date) {
+    if (date == null || date.getClass() == Date.class) {
+      return date;
+    }
+    return new Date(date.getTime());
+  }
+
   /**
    * Convert an object to the data type specified in the conversion metadata
    *
diff --git 
a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaTimestampTest.java 
b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaTimestampTest.java
index 3513023f2f..f36de4c9c7 100644
--- 
a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaTimestampTest.java
+++ 
b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaTimestampTest.java
@@ -18,12 +18,16 @@ package org.apache.hop.core.row.value;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 import java.sql.PreparedStatement;
 import java.sql.Timestamp;
@@ -46,10 +50,111 @@ class ValueMetaTimestampTest {
         ValueMetaBase.DEFAULT_TIMESTAMP_FORMAT_MASK, 
ValueMetaBase.DEFAULT_TIMESTAMP_PARSE_MASK);
   }
 
+  @Test
+  void testConvertTimestampToDateReturnsDateInsteadOfTimestamp() throws 
Exception {
+    ValueMetaTimestamp valueMetaTimestamp = new ValueMetaTimestamp();
+    ValueMetaDate valueMetaDate = new ValueMetaDate();
+
+    for (String value :
+        new String[] {
+          "1960-01-02 03:04:05.987654321",
+          "1970-01-01 00:00:00.000000000",
+          "2025-08-28 08:14:13.123456789"
+        }) {
+      Timestamp timestamp = Timestamp.valueOf(value);
+
+      Date date = (Date) valueMetaDate.convertData(valueMetaTimestamp, 
timestamp);
+
+      assertEquals(Date.class, date.getClass());
+      assertEquals(timestamp.getTime(), date.getTime());
+      assertNotSame(timestamp, date);
+    }
+  }
+
+  @Test
+  void testConvertNullTimestampToDateReturnsNull() throws Exception {
+    ValueMetaTimestamp valueMetaTimestamp = new ValueMetaTimestamp();
+    ValueMetaDate valueMetaDate = new ValueMetaDate();
+
+    assertNull(valueMetaDate.convertData(valueMetaTimestamp, null));
+  }
+
+  @Test
+  void testConvertDateToDateKeepsPlainDateInstance() throws Exception {
+    ValueMetaDate valueMetaDate = new ValueMetaDate();
+    Date date = new Date(1_756_369_253_123L);
+
+    assertSame(date, valueMetaDate.convertData(valueMetaDate, date));
+  }
+
+  @Test
+  void testConvertSqlDateToDateReturnsPlainDate() throws Exception {
+    ValueMetaDate valueMetaDate = new ValueMetaDate();
+    java.sql.Date sqlDate = new java.sql.Date(1_756_369_253_123L);
+
+    Date date = (Date) valueMetaDate.convertData(valueMetaDate, sqlDate);
+
+    assertEquals(Date.class, date.getClass());
+    assertEquals(sqlDate.getTime(), date.getTime());
+    assertNotSame(sqlDate, date);
+  }
+
+  @Test
+  void testCompatibleConversionToDateReturnsPlainDate() throws Exception {
+    ValueMetaTimestamp valueMetaTimestamp = new ValueMetaTimestamp();
+    ValueMetaDate valueMetaDate = new ValueMetaDate();
+    Timestamp timestamp = Timestamp.valueOf("2025-08-28 08:14:13.123456789");
+
+    Date fromTimestamp = (Date) 
valueMetaDate.convertDataCompatible(valueMetaTimestamp, timestamp);
+    assertEquals(Date.class, fromTimestamp.getClass());
+    assertEquals(timestamp.getTime(), fromTimestamp.getTime());
+    assertNotSame(timestamp, fromTimestamp);
+
+    // A Date field can already hold a Timestamp, for example after a JDBC 
read.
+    Date fromDateField = (Date) 
valueMetaDate.convertDataCompatible(valueMetaDate, timestamp);
+    assertEquals(Date.class, fromDateField.getClass());
+    assertEquals(timestamp.getTime(), fromDateField.getTime());
+    assertNotSame(timestamp, fromDateField);
+
+    assertNull(valueMetaDate.convertDataCompatible(valueMetaTimestamp, null));
+
+    Date plain = new Date(1_756_369_253_123L);
+    assertSame(plain, valueMetaDate.convertDataCompatible(valueMetaDate, 
plain));
+
+    java.sql.Date sqlDate = new java.sql.Date(1_756_369_253_123L);
+    Date fromSql = (Date) valueMetaDate.convertDataCompatible(valueMetaDate, 
sqlDate);
+    assertEquals(Date.class, fromSql.getClass());
+    assertEquals(sqlDate.getTime(), fromSql.getTime());
+    assertNotSame(sqlDate, fromSql);
+  }
+
+  @Test
+  void testConvertDataOfDateFieldHoldingTimestampReturnsPlainDate() throws 
Exception {
+    ValueMetaDate valueMetaDate = new ValueMetaDate();
+    Timestamp timestamp = Timestamp.valueOf("2025-08-28 08:14:13.123456789");
+
+    Date date = (Date) valueMetaDate.convertData(valueMetaDate, timestamp);
+
+    assertEquals(Date.class, date.getClass());
+    assertEquals(timestamp.getTime(), date.getTime());
+    assertNotSame(timestamp, date);
+  }
+
+  @Test
+  void testTimestampConversionRemainsUnchanged() throws Exception {
+    ValueMetaTimestamp valueMetaTimestamp = new ValueMetaTimestamp();
+    Timestamp timestamp = Timestamp.valueOf("2025-08-28 08:14:13.123456789");
+
+    assertSame(timestamp, valueMetaTimestamp.getDate(timestamp));
+    assertSame(timestamp, valueMetaTimestamp.convertData(valueMetaTimestamp, 
timestamp));
+    assertEquals(123456789, timestamp.getNanos());
+  }
+
   @Test
   void testSetPreparedStatementValue() throws Exception {
     ValueMetaTimestamp vm = new ValueMetaTimestamp();
     PreparedStatement ps = mock(PreparedStatement.class);
+    Timestamp timestamp = Timestamp.valueOf("2025-08-28 08:14:13.123456789");
     doAnswer(
             (Answer<Object>)
                 invocationOnMock -> {
@@ -60,7 +165,10 @@ class ValueMetaTimestampTest {
         .setTimestamp(anyInt(), (Timestamp) any());
 
     try {
-      vm.setPreparedStatementValue(mock(DatabaseMeta.class), ps, 0, null);
+      vm.setPreparedStatementValue(mock(DatabaseMeta.class), ps, 1, timestamp);
+      verify(ps).setTimestamp(1, timestamp);
+      vm.setPreparedStatementValue(mock(DatabaseMeta.class), ps, 2, null);
+      verify(ps).setNull(2, java.sql.Types.TIMESTAMP);
     } catch (HopDatabaseException ex) {
       fail("Error setting value on prepared statement ");
     }

Reply via email to