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 ");
}