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

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 67d0600  NIFI-8023: Convert java.sql.Date between UTC/local time zone 
normalized forms before/after database operations
67d0600 is described below

commit 67d06003b7f8a4332e72dd54814cb5fb8bed9989
Author: Peter Turcsanyi <[email protected]>
AuthorDate: Mon Jan 25 21:15:26 2021 +0000

    NIFI-8023: Convert java.sql.Date between UTC/local time zone normalized 
forms before/after database operations
    
    This closes #4781
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../serialization/record/ResultSetRecordSet.java   |   7 +
 .../serialization/record/util/DataTypeUtils.java   |  30 ++++
 .../record/ResultSetRecordSetTest.java             | 168 ++++++++++++++++-----
 .../serialization/record/TestDataTypeUtils.java    |  45 ++++++
 .../java/org/apache/nifi/util/db/JdbcCommon.java   |  13 ++
 .../org/apache/nifi/util/db/TestJdbcCommon.java    |  12 +-
 .../org/apache/nifi/avro/TestAvroTypeUtil.java     |   6 +-
 .../processors/standard/PutDatabaseRecord.java     |   9 +-
 .../standard/TestPutDatabaseRecord.groovy          |  46 ++++--
 9 files changed, 277 insertions(+), 59 deletions(-)

diff --git 
a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/ResultSetRecordSet.java
 
b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/ResultSetRecordSet.java
index 9b83f1a..81a6a9c 100644
--- 
a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/ResultSetRecordSet.java
+++ 
b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/ResultSetRecordSet.java
@@ -18,6 +18,7 @@
 package org.apache.nifi.serialization.record;
 
 import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.util.DataTypeUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -149,6 +150,12 @@ public class ResultSetRecordSet implements RecordSet, 
Closeable {
             return null;
         }
 
+        if (value instanceof java.sql.Date) {
+            // Date objects should be stored in records as UTC normalized 
dates (UTC 00:00:00)
+            // but they come from the driver in JVM's local time zone 00:00:00 
and need to be converted.
+            return DataTypeUtils.convertDateToUTC((java.sql.Date) value);
+        }
+
         if (value instanceof List) {
             return ((List) value).toArray();
         }
diff --git 
a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java
 
b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java
index a4c9462..2bfa6cf 100644
--- 
a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java
+++ 
b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java
@@ -49,6 +49,10 @@ import java.sql.Types;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -1085,6 +1089,32 @@ public class DataTypeUtils {
         throw new IllegalTypeConversionException("Cannot convert value [" + 
value + "] of type " + value.getClass() + " to Date for field " + fieldName);
     }
 
+    /**
+     * Converts a java.sql.Date object in local time zone (typically coming 
from a java.sql.ResultSet and having 00:00:00 time part)
+     * to UTC normalized form (storing the epoch corresponding to the UTC time 
with the same date/time as the input).
+     *
+     * @param dateLocalTZ java.sql.Date in local time zone
+     * @return java.sql.Date in UTC normalized form
+     */
+    public static Date convertDateToUTC(Date dateLocalTZ) {
+        ZonedDateTime zdtLocalTZ = 
ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateLocalTZ.getTime()), 
ZoneId.systemDefault());
+        ZonedDateTime zdtUTC = zdtLocalTZ.withZoneSameLocal(ZoneOffset.UTC);
+        return new Date(zdtUTC.toInstant().toEpochMilli());
+    }
+
+    /**
+     * Converts a java.sql.Date object in UTC normalized form
+     * to local time zone (storing the epoch corresponding to the local time 
with the same date/time as the input).
+     *
+     * @param dateUTC java.sql.Date in UTC normalized form
+     * @return java.sql.Date in local time zone
+     */
+    public static Date convertDateToLocalTZ(Date dateUTC) {
+        ZonedDateTime zdtUTC = 
ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateUTC.getTime()), 
ZoneOffset.UTC);
+        ZonedDateTime zdtLocalTZ = 
zdtUTC.withZoneSameLocal(ZoneId.systemDefault());
+        return new Date(zdtLocalTZ.toInstant().toEpochMilli());
+    }
+
     public static boolean isDateTypeCompatible(final Object value, final 
String format) {
         if (value == null) {
             return false;
diff --git 
a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/ResultSetRecordSetTest.java
 
b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/ResultSetRecordSetTest.java
index 019e01c..2690dae 100644
--- 
a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/ResultSetRecordSetTest.java
+++ 
b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/ResultSetRecordSetTest.java
@@ -18,7 +18,6 @@ package org.apache.nifi.serialization.record;
 
 import org.apache.nifi.serialization.SimpleRecordSchema;
 import org.apache.nifi.serialization.record.type.DecimalDataType;
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -27,35 +26,61 @@ import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
 
 import java.math.BigDecimal;
+import java.sql.Date;
 import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
 import java.sql.Types;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.when;
+
 @RunWith(MockitoJUnitRunner.class)
 public class ResultSetRecordSetTest {
+
+    private static final String COLUMN_NAME_VARCHAR = "varchar";
+    private static final String COLUMN_NAME_BIGINT = "bigint";
+    private static final String COLUMN_NAME_ROWID = "rowid";
+    private static final String COLUMN_NAME_BIT = "bit";
+    private static final String COLUMN_NAME_BOOLEAN = "boolean";
+    private static final String COLUMN_NAME_CHAR = "char";
+    private static final String COLUMN_NAME_DATE = "date";
+    private static final String COLUMN_NAME_INTEGER = "integer";
+    private static final String COLUMN_NAME_DOUBLE = "double";
+    private static final String COLUMN_NAME_REAL = "real";
+    private static final String COLUMN_NAME_FLOAT = "float";
+    private static final String COLUMN_NAME_SMALLINT = "smallint";
+    private static final String COLUMN_NAME_TINYINT = "tinyint";
+    private static final String COLUMN_NAME_BIG_DECIMAL_1 = "bigDecimal1";
+    private static final String COLUMN_NAME_BIG_DECIMAL_2 = "bigDecimal2";
+    private static final String COLUMN_NAME_BIG_DECIMAL_3 = "bigDecimal3";
+    private static final String COLUMN_NAME_BIG_DECIMAL_4 = "bigDecimal4";
+
     private static final Object[][] COLUMNS = new Object[][] {
             // column number; column label / name / schema field; column type; 
schema data type;
-            {1, "varchar", Types.VARCHAR, 
RecordFieldType.STRING.getDataType()},
-            {2, "bigint", Types.BIGINT, RecordFieldType.LONG.getDataType()},
-            {3, "rowid", Types.ROWID, RecordFieldType.LONG.getDataType()},
-            {4, "bit", Types.BIT, RecordFieldType.BOOLEAN.getDataType()},
-            {5, "boolean", Types.BOOLEAN, 
RecordFieldType.BOOLEAN.getDataType()},
-            {6, "char", Types.CHAR, RecordFieldType.CHAR.getDataType()},
-            {7, "date", Types.DATE, RecordFieldType.DATE.getDataType()},
-            {8, "integer", Types.INTEGER, RecordFieldType.INT.getDataType()},
-            {9, "double", Types.DOUBLE, RecordFieldType.DOUBLE.getDataType()},
-            {10, "real", Types.REAL, RecordFieldType.DOUBLE.getDataType()},
-            {11, "float", Types.FLOAT, RecordFieldType.FLOAT.getDataType()},
-            {12, "smallint", Types.SMALLINT, 
RecordFieldType.SHORT.getDataType()},
-            {13, "tinyint", Types.TINYINT, RecordFieldType.BYTE.getDataType()},
-            {14, "bigDecimal1", 
Types.DECIMAL,RecordFieldType.DECIMAL.getDecimalDataType(7, 3)},
-            {15, "bigDecimal2", Types.NUMERIC, 
RecordFieldType.DECIMAL.getDecimalDataType(4, 0)},
-            {16, "bigDecimal3", Types.JAVA_OBJECT, 
RecordFieldType.DECIMAL.getDecimalDataType(501, 1)},
-            {17, "bigDecimal4", Types.DECIMAL, 
RecordFieldType.DECIMAL.getDecimalDataType(10, 3)},
+            {1, COLUMN_NAME_VARCHAR, Types.VARCHAR, 
RecordFieldType.STRING.getDataType()},
+            {2, COLUMN_NAME_BIGINT, Types.BIGINT, 
RecordFieldType.LONG.getDataType()},
+            {3, COLUMN_NAME_ROWID, Types.ROWID, 
RecordFieldType.LONG.getDataType()},
+            {4, COLUMN_NAME_BIT, Types.BIT, 
RecordFieldType.BOOLEAN.getDataType()},
+            {5, COLUMN_NAME_BOOLEAN, Types.BOOLEAN, 
RecordFieldType.BOOLEAN.getDataType()},
+            {6, COLUMN_NAME_CHAR, Types.CHAR, 
RecordFieldType.CHAR.getDataType()},
+            {7, COLUMN_NAME_DATE, Types.DATE, 
RecordFieldType.DATE.getDataType()},
+            {8, COLUMN_NAME_INTEGER, Types.INTEGER, 
RecordFieldType.INT.getDataType()},
+            {9, COLUMN_NAME_DOUBLE, Types.DOUBLE, 
RecordFieldType.DOUBLE.getDataType()},
+            {10, COLUMN_NAME_REAL, Types.REAL, 
RecordFieldType.DOUBLE.getDataType()},
+            {11, COLUMN_NAME_FLOAT, Types.FLOAT, 
RecordFieldType.FLOAT.getDataType()},
+            {12, COLUMN_NAME_SMALLINT, Types.SMALLINT, 
RecordFieldType.SHORT.getDataType()},
+            {13, COLUMN_NAME_TINYINT, Types.TINYINT, 
RecordFieldType.BYTE.getDataType()},
+            {14, COLUMN_NAME_BIG_DECIMAL_1, 
Types.DECIMAL,RecordFieldType.DECIMAL.getDecimalDataType(7, 3)},
+            {15, COLUMN_NAME_BIG_DECIMAL_2, Types.NUMERIC, 
RecordFieldType.DECIMAL.getDecimalDataType(4, 0)},
+            {16, COLUMN_NAME_BIG_DECIMAL_3, Types.JAVA_OBJECT, 
RecordFieldType.DECIMAL.getDecimalDataType(501, 1)},
+            {17, COLUMN_NAME_BIG_DECIMAL_4, Types.DECIMAL, 
RecordFieldType.DECIMAL.getDecimalDataType(10, 3)},
     };
 
     @Mock
@@ -66,26 +91,26 @@ public class ResultSetRecordSetTest {
 
     @Before
     public void setUp() throws SQLException {
-        Mockito.when(resultSet.getMetaData()).thenReturn(resultSetMetaData);
-        
Mockito.when(resultSetMetaData.getColumnCount()).thenReturn(COLUMNS.length);
+        when(resultSet.getMetaData()).thenReturn(resultSetMetaData);
+        when(resultSetMetaData.getColumnCount()).thenReturn(COLUMNS.length);
 
         for (final Object[] column : COLUMNS) {
-            Mockito.when(resultSetMetaData.getColumnLabel((Integer) 
column[0])).thenReturn((column[1]) + "Col");
-            Mockito.when(resultSetMetaData.getColumnName((Integer) 
column[0])).thenReturn((String) column[1]);
-            Mockito.when(resultSetMetaData.getColumnType((Integer) 
column[0])).thenReturn((Integer) column[2]);
+            when(resultSetMetaData.getColumnLabel((Integer) 
column[0])).thenReturn((String) (column[1]));
+            when(resultSetMetaData.getColumnName((Integer) 
column[0])).thenReturn((String) column[1]);
+            when(resultSetMetaData.getColumnType((Integer) 
column[0])).thenReturn((Integer) column[2]);
 
             if(column[3] instanceof DecimalDataType) {
                 DecimalDataType ddt = (DecimalDataType)column[3];
-                Mockito.when(resultSetMetaData.getPrecision((Integer) 
column[0])).thenReturn(ddt.getPrecision());
-                Mockito.when(resultSetMetaData.getScale((Integer) 
column[0])).thenReturn(ddt.getScale());
+                when(resultSetMetaData.getPrecision((Integer) 
column[0])).thenReturn(ddt.getPrecision());
+                when(resultSetMetaData.getScale((Integer) 
column[0])).thenReturn(ddt.getScale());
             }
         }
 
         // Big decimal values are necessary in order to determine precision 
and scale
-        Mockito.when(resultSet.getBigDecimal(16)).thenReturn(new 
BigDecimal(String.join("", Collections.nCopies(500, "1")) + ".1"));
+        when(resultSet.getBigDecimal(16)).thenReturn(new 
BigDecimal(String.join("", Collections.nCopies(500, "1")) + ".1"));
 
         // This will be handled by a dedicated branch for Java Objects, needs 
some further details
-        
Mockito.when(resultSetMetaData.getColumnClassName(16)).thenReturn(BigDecimal.class.getName());
+        
when(resultSetMetaData.getColumnClassName(16)).thenReturn(BigDecimal.class.getName());
     }
 
     @Test
@@ -124,7 +149,7 @@ public class ResultSetRecordSetTest {
         final RecordSchema resultSchema = testSubject.getSchema();
 
         // then
-        Assert.assertEquals(RecordFieldType.DECIMAL.getDecimalDataType(30, 
10), resultSchema.getField(0).getDataType());
+        assertEquals(RecordFieldType.DECIMAL.getDecimalDataType(30, 10), 
resultSchema.getField(0).getDataType());
     }
 
     @Test
@@ -137,17 +162,88 @@ public class ResultSetRecordSetTest {
         final RecordSchema resultSchema = testSubject.getSchema();
 
         // then
-        Assert.assertEquals(RecordFieldType.CHOICE, 
resultSchema.getField(0).getDataType().getFieldType());
+        assertEquals(RecordFieldType.CHOICE, 
resultSchema.getField(0).getDataType().getFieldType());
+    }
+
+    @Test
+    public void testCreateRecord() throws SQLException {
+        // given
+        final RecordSchema recordSchema = givenRecordSchema();
+
+        LocalDate testDate = LocalDate.of(2021, 1, 26);
+
+        final String varcharValue = "varchar";
+        final Long bigintValue = 1234567890123456789L;
+        final Long rowidValue = 11111111L;
+        final Boolean bitValue = Boolean.FALSE;
+        final Boolean booleanValue = Boolean.TRUE;
+        final Character charValue = 'c';
+        final Date dateValue = Date.valueOf(testDate);
+        final Integer integerValue = 1234567890;
+        final Double doubleValue = 0.12;
+        final Double realValue = 3.45;
+        final Float floatValue = 6.78F;
+        final Short smallintValue = 12345;
+        final Byte tinyintValue = 123;
+        final BigDecimal bigDecimal1Value = new BigDecimal("1234.567");
+        final BigDecimal bigDecimal2Value = new BigDecimal("1234");
+        final BigDecimal bigDecimal3Value = new BigDecimal("1234567890.1");
+        final BigDecimal bigDecimal4Value = new BigDecimal("1234567.089");
+
+        
when(resultSet.getObject(COLUMN_NAME_VARCHAR)).thenReturn(varcharValue);
+        when(resultSet.getObject(COLUMN_NAME_BIGINT)).thenReturn(bigintValue);
+        when(resultSet.getObject(COLUMN_NAME_ROWID)).thenReturn(rowidValue);
+        when(resultSet.getObject(COLUMN_NAME_BIT)).thenReturn(bitValue);
+        
when(resultSet.getObject(COLUMN_NAME_BOOLEAN)).thenReturn(booleanValue);
+        when(resultSet.getObject(COLUMN_NAME_CHAR)).thenReturn(charValue);
+        when(resultSet.getObject(COLUMN_NAME_DATE)).thenReturn(dateValue);
+        
when(resultSet.getObject(COLUMN_NAME_INTEGER)).thenReturn(integerValue);
+        when(resultSet.getObject(COLUMN_NAME_DOUBLE)).thenReturn(doubleValue);
+        when(resultSet.getObject(COLUMN_NAME_REAL)).thenReturn(realValue);
+        when(resultSet.getObject(COLUMN_NAME_FLOAT)).thenReturn(floatValue);
+        
when(resultSet.getObject(COLUMN_NAME_SMALLINT)).thenReturn(smallintValue);
+        
when(resultSet.getObject(COLUMN_NAME_TINYINT)).thenReturn(tinyintValue);
+        
when(resultSet.getObject(COLUMN_NAME_BIG_DECIMAL_1)).thenReturn(bigDecimal1Value);
+        
when(resultSet.getObject(COLUMN_NAME_BIG_DECIMAL_2)).thenReturn(bigDecimal2Value);
+        
when(resultSet.getObject(COLUMN_NAME_BIG_DECIMAL_3)).thenReturn(bigDecimal3Value);
+        
when(resultSet.getObject(COLUMN_NAME_BIG_DECIMAL_4)).thenReturn(bigDecimal4Value);
+
+        // when
+        ResultSetRecordSet testSubject = new ResultSetRecordSet(resultSet, 
recordSchema);
+        Record record = testSubject.createRecord(resultSet);
+
+        // then
+        assertEquals(varcharValue, record.getAsString(COLUMN_NAME_VARCHAR));
+        assertEquals(bigintValue, record.getAsLong(COLUMN_NAME_BIGINT));
+        assertEquals(rowidValue, record.getAsLong(COLUMN_NAME_ROWID));
+        assertEquals(bitValue, record.getAsBoolean(COLUMN_NAME_BIT));
+        assertEquals(booleanValue, record.getAsBoolean(COLUMN_NAME_BOOLEAN));
+        assertEquals(charValue, record.getValue(COLUMN_NAME_CHAR));
+
+        // Date is expected in UTC normalized form
+        Date expectedDate = new 
Date(testDate.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli());
+        assertEquals(expectedDate, record.getAsDate(COLUMN_NAME_DATE, null));
+
+        assertEquals(integerValue, record.getAsInt(COLUMN_NAME_INTEGER));
+        assertEquals(doubleValue, record.getAsDouble(COLUMN_NAME_DOUBLE));
+        assertEquals(realValue, record.getAsDouble(COLUMN_NAME_REAL));
+        assertEquals(floatValue, record.getAsFloat(COLUMN_NAME_FLOAT));
+        assertEquals(smallintValue.shortValue(), 
record.getAsInt(COLUMN_NAME_SMALLINT).shortValue());
+        assertEquals(tinyintValue.byteValue(), 
record.getAsInt(COLUMN_NAME_TINYINT).byteValue());
+        assertEquals(bigDecimal1Value, 
record.getValue(COLUMN_NAME_BIG_DECIMAL_1));
+        assertEquals(bigDecimal2Value, 
record.getValue(COLUMN_NAME_BIG_DECIMAL_2));
+        assertEquals(bigDecimal3Value, 
record.getValue(COLUMN_NAME_BIG_DECIMAL_3));
+        assertEquals(bigDecimal4Value, 
record.getValue(COLUMN_NAME_BIG_DECIMAL_4));
     }
 
     private ResultSet givenResultSetForOther() throws SQLException {
         final ResultSet resultSet = Mockito.mock(ResultSet.class);
         final ResultSetMetaData resultSetMetaData = 
Mockito.mock(ResultSetMetaData.class);
-        Mockito.when(resultSet.getMetaData()).thenReturn(resultSetMetaData);
-        Mockito.when(resultSetMetaData.getColumnCount()).thenReturn(1);
-        Mockito.when(resultSetMetaData.getColumnLabel(1)).thenReturn("column");
-        Mockito.when(resultSetMetaData.getColumnName(1)).thenReturn("column");
-        
Mockito.when(resultSetMetaData.getColumnType(1)).thenReturn(Types.OTHER);
+        when(resultSet.getMetaData()).thenReturn(resultSetMetaData);
+        when(resultSetMetaData.getColumnCount()).thenReturn(1);
+        when(resultSetMetaData.getColumnLabel(1)).thenReturn("column");
+        when(resultSetMetaData.getColumnName(1)).thenReturn("column");
+        when(resultSetMetaData.getColumnType(1)).thenReturn(Types.OTHER);
         return resultSet;
     }
 
@@ -162,10 +258,10 @@ public class ResultSetRecordSetTest {
     }
 
     private void thenAllColumnDataTypesAreCorrect(final RecordSchema 
resultSchema) {
-        Assert.assertNotNull(resultSchema);
+        assertNotNull(resultSchema);
 
         for (final Object[] column : COLUMNS) {
-            Assert.assertEquals("For column " + column[0] + " the converted 
type is not matching", column[3], resultSchema.getField((Integer) column[0] - 
1).getDataType());
+            assertEquals("For column " + column[0] + " the converted type is 
not matching", column[3], resultSchema.getField((Integer) column[0] - 
1).getDataType());
         }
     }
 }
\ No newline at end of file
diff --git 
a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/TestDataTypeUtils.java
 
b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/TestDataTypeUtils.java
index 017763c..c89e656 100644
--- 
a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/TestDataTypeUtils.java
+++ 
b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/TestDataTypeUtils.java
@@ -27,8 +27,13 @@ import org.junit.Test;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.nio.charset.StandardCharsets;
+import java.sql.Date;
 import java.sql.Timestamp;
 import java.sql.Types;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -874,4 +879,44 @@ public class TestDataTypeUtils {
         assertTrue(DataTypeUtils.isFittingNumberType(9D, 
RecordFieldType.DOUBLE));
         assertFalse(DataTypeUtils.isFittingNumberType(9, 
RecordFieldType.DOUBLE));
     }
+
+    @Test
+    public void testConvertDateToUTC() {
+        int year = 2021;
+        int month = 1;
+        int dayOfMonth = 25;
+
+        Date dateLocalTZ = new Date(ZonedDateTime.of(LocalDateTime.of(year, 
month, dayOfMonth,0,0,0), ZoneId.systemDefault()).toInstant().toEpochMilli());
+
+        Date dateUTC = DataTypeUtils.convertDateToUTC(dateLocalTZ);
+
+        ZonedDateTime zdt = 
ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateUTC.getTime()), 
ZoneId.of("UTC"));
+        assertEquals(year, zdt.getYear());
+        assertEquals(month, zdt.getMonthValue());
+        assertEquals(dayOfMonth, zdt.getDayOfMonth());
+        assertEquals(0, zdt.getHour());
+        assertEquals(0, zdt.getMinute());
+        assertEquals(0, zdt.getSecond());
+        assertEquals(0, zdt.getNano());
+    }
+
+    @Test
+    public void testConvertDateToLocalTZ() {
+        int year = 2021;
+        int month = 1;
+        int dayOfMonth = 25;
+
+        Date dateUTC = new Date(ZonedDateTime.of(LocalDateTime.of(year, month, 
dayOfMonth,0,0,0), ZoneId.of("UTC")).toInstant().toEpochMilli());
+
+        Date dateLocalTZ = DataTypeUtils.convertDateToLocalTZ(dateUTC);
+
+        ZonedDateTime zdt = 
ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateLocalTZ.getTime()), 
ZoneId.systemDefault());
+        assertEquals(year, zdt.getYear());
+        assertEquals(month, zdt.getMonthValue());
+        assertEquals(dayOfMonth, zdt.getDayOfMonth());
+        assertEquals(0, zdt.getHour());
+        assertEquals(0, zdt.getMinute());
+        assertEquals(0, zdt.getSecond());
+        assertEquals(0, zdt.getNano());
+    }
 }
diff --git 
a/nifi-nar-bundles/nifi-extension-utils/nifi-database-utils/src/main/java/org/apache/nifi/util/db/JdbcCommon.java
 
b/nifi-nar-bundles/nifi-extension-utils/nifi-database-utils/src/main/java/org/apache/nifi/util/db/JdbcCommon.java
index 26800d2..c5e454a 100644
--- 
a/nifi-nar-bundles/nifi-extension-utils/nifi-database-utils/src/main/java/org/apache/nifi/util/db/JdbcCommon.java
+++ 
b/nifi-nar-bundles/nifi-extension-utils/nifi-database-utils/src/main/java/org/apache/nifi/util/db/JdbcCommon.java
@@ -100,6 +100,7 @@ import org.apache.avro.io.DatumWriter;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.avro.AvroTypeUtil;
+import org.apache.nifi.serialization.record.util.DataTypeUtils;
 
 import javax.xml.bind.DatatypeConverter;
 
@@ -393,6 +394,18 @@ public class JdbcCommon {
                             rec.put(i-1, value);
                         }
 
+                    } else if (value instanceof java.sql.Date) {
+                        if (options.useLogicalTypes) {
+                            // Delegate mapping to AvroTypeUtil in order to 
utilize logical types.
+                            // AvroTypeUtil.convertToAvroObject() expects 
java.sql.Date object as a UTC normalized date (UTC 00:00:00)
+                            // but it comes from the driver in JVM's local 
time zone 00:00:00 and needs to be converted.
+                            java.sql.Date normalizedDate = 
DataTypeUtils.convertDateToUTC((java.sql.Date) value);
+                            rec.put(i - 1, 
AvroTypeUtil.convertToAvroObject(normalizedDate, fieldSchema));
+                        } else {
+                            // As string for backward compatibility.
+                            rec.put(i - 1, value.toString());
+                        }
+
                     } else if (value instanceof Date) {
                         if (options.useLogicalTypes) {
                             // Delegate mapping to AvroTypeUtil in order to 
utilize logical types.
diff --git 
a/nifi-nar-bundles/nifi-extension-utils/nifi-database-utils/src/test/java/org/apache/nifi/util/db/TestJdbcCommon.java
 
b/nifi-nar-bundles/nifi-extension-utils/nifi-database-utils/src/test/java/org/apache/nifi/util/db/TestJdbcCommon.java
index bbf2bdb..cc51889 100644
--- 
a/nifi-nar-bundles/nifi-extension-utils/nifi-database-utils/src/test/java/org/apache/nifi/util/db/TestJdbcCommon.java
+++ 
b/nifi-nar-bundles/nifi-extension-utils/nifi-database-utils/src/test/java/org/apache/nifi/util/db/TestJdbcCommon.java
@@ -59,6 +59,7 @@ import java.sql.Time;
 import java.sql.Timestamp;
 import java.sql.Types;
 import java.time.Instant;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.ZoneId;
@@ -67,10 +68,10 @@ import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
 import java.time.temporal.ChronoField;
+import java.time.temporal.ChronoUnit;
 import java.time.temporal.TemporalAccessor;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
@@ -679,11 +680,10 @@ public class TestJdbcCommon {
 
         testConvertToAvroStreamForDateTime(options,
                 (record, date) -> {
-                    final int daysSinceEpoch = (int) record.get("date");
-                    final long millisSinceEpoch = 
TimeUnit.MILLISECONDS.convert(daysSinceEpoch, TimeUnit.DAYS);
-                    java.sql.Date actual = 
java.sql.Date.valueOf(Instant.ofEpochMilli(millisSinceEpoch).atZone(ZoneOffset.UTC).toLocalDate());
-                    LOGGER.debug("comparing dates, expecting '{}', actual 
'{}'", date, actual);
-                    assertEquals(date, actual);
+                    final int expectedDaysSinceEpoch = (int) 
ChronoUnit.DAYS.between(LocalDate.ofEpochDay(0), date.toLocalDate());
+                    final int actualDaysSinceEpoch = (int) record.get("date");
+                    LOGGER.debug("comparing days since epoch, expecting '{}', 
actual '{}'", expectedDaysSinceEpoch, actualDaysSinceEpoch);
+                    assertEquals(expectedDaysSinceEpoch, actualDaysSinceEpoch);
                 },
                 (record, time) -> {
                     int millisSinceMidnight = (int) record.get("time");
diff --git 
a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/test/java/org/apache/nifi/avro/TestAvroTypeUtil.java
 
b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/test/java/org/apache/nifi/avro/TestAvroTypeUtil.java
index 6cc6f4f..9ca4a5a 100644
--- 
a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/test/java/org/apache/nifi/avro/TestAvroTypeUtil.java
+++ 
b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/test/java/org/apache/nifi/avro/TestAvroTypeUtil.java
@@ -56,6 +56,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.TimeZone;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -483,8 +484,9 @@ public class TestAvroTypeUtil {
 
     @Test
     public void testDateConversion() {
-        final Calendar c = Calendar.getInstance();
+        final Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
         c.set(2019, Calendar.JANUARY, 1, 0, 0, 0);
+        c.set(Calendar.MILLISECOND, 0);
         final long epochMillis = c.getTimeInMillis();
 
         final LogicalTypes.Date dateType = LogicalTypes.date();
@@ -492,7 +494,7 @@ public class TestAvroTypeUtil {
         dateType.addToSchema(fieldSchema);
         final Object convertedValue = AvroTypeUtil.convertToAvroObject(new 
Date(epochMillis), fieldSchema);
         assertTrue(convertedValue instanceof Integer);
-        assertEquals((int) convertedValue, LocalDate.of(2019, 1, 
1).toEpochDay());
+        assertEquals(LocalDate.of(2019, 1, 1).toEpochDay(), (int) 
convertedValue);
     }
 
     @Test
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/PutDatabaseRecord.java
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/PutDatabaseRecord.java
index 11049f9..849c88d 100644
--- 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/PutDatabaseRecord.java
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/PutDatabaseRecord.java
@@ -63,6 +63,7 @@ import java.io.InputStream;
 import java.sql.BatchUpdateException;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
+import java.sql.Date;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
@@ -71,6 +72,7 @@ import java.sql.SQLException;
 import java.sql.SQLIntegrityConstraintViolationException;
 import java.sql.SQLTransientException;
 import java.sql.Statement;
+import java.sql.Types;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -685,10 +687,15 @@ public class PutDatabaseRecord extends AbstractProcessor {
 
                     for (int i = 0; i < fieldIndexes.size(); i++) {
                         final int currentFieldIndex = fieldIndexes.get(i);
-                        final Object currentValue = values[currentFieldIndex];
+                        Object currentValue = values[currentFieldIndex];
                         final DataType dataType = 
dataTypes.get(currentFieldIndex);
                         final int sqlType = 
DataTypeUtils.getSQLTypeValue(dataType);
 
+                        if (sqlType == Types.DATE && currentValue instanceof 
Date) {
+                            // convert Date from the internal UTC normalized 
form to local time zone needed by database drivers
+                            currentValue = 
DataTypeUtils.convertDateToLocalTZ((Date) currentValue);
+                        }
+
                         // If DELETE type, insert the object twice because of 
the null check (see generateDelete for details)
                         if (DELETE_TYPE.equalsIgnoreCase(statementType)) {
                             ps.setObject(i * 2 + 1, currentValue, sqlType);
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/TestPutDatabaseRecord.groovy
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/TestPutDatabaseRecord.groovy
index a219a80..558eb2f 100644
--- 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/TestPutDatabaseRecord.groovy
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/TestPutDatabaseRecord.groovy
@@ -39,6 +39,7 @@ import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 import java.sql.Connection
+import java.sql.Date
 import java.sql.DriverManager
 import java.sql.PreparedStatement
 import java.sql.ResultSet
@@ -46,6 +47,9 @@ import java.sql.SQLDataException
 import java.sql.SQLException
 import java.sql.SQLNonTransientConnectionException
 import java.sql.Statement
+import java.time.LocalDate
+import java.time.ZoneId
+import java.time.ZoneOffset
 import java.util.function.Supplier
 
 import static org.junit.Assert.assertEquals
@@ -68,7 +72,8 @@ import static org.mockito.Mockito.verify
 class TestPutDatabaseRecord {
 
     private static final String createPersons = "CREATE TABLE PERSONS (id 
integer primary key, name varchar(100)," +
-            " code integer CONSTRAINT CODE_RANGE CHECK (code >= 0 AND code < 
1000))"
+            " code integer CONSTRAINT CODE_RANGE CHECK (code >= 0 AND code < 
1000)," +
+            " dt date)"
     private final static String DB_LOCATION = "target/db_pdr"
 
     TestRunner runner
@@ -238,12 +243,20 @@ class TestPutDatabaseRecord {
         parser.addSchemaField("id", RecordFieldType.INT)
         parser.addSchemaField("name", RecordFieldType.STRING)
         parser.addSchemaField("code", RecordFieldType.INT)
+        parser.addSchemaField("dt", RecordFieldType.DATE)
 
-        parser.addRecord(1, 'rec1', 101)
-        parser.addRecord(2, 'rec2', 102)
-        parser.addRecord(3, 'rec3', 103)
-        parser.addRecord(4, 'rec4', 104)
-        parser.addRecord(5, null, 105)
+        LocalDate testDate1 = LocalDate.of(2021, 1, 26)
+        Date nifiDate1 = new 
Date(testDate1.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()) // in 
UTC
+        Date jdbcDate1 = Date.valueOf(testDate1) // in local TZ
+        LocalDate testDate2 = LocalDate.of(2021, 7, 26)
+        Date nifiDate2 = new 
Date(testDate2.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()) // in 
URC
+        Date jdbcDate2 = Date.valueOf(testDate2) // in local TZ
+
+        parser.addRecord(1, 'rec1', 101, nifiDate1)
+        parser.addRecord(2, 'rec2', 102, nifiDate2)
+        parser.addRecord(3, 'rec3', 103, null)
+        parser.addRecord(4, 'rec4', 104, null)
+        parser.addRecord(5, null, 105, null)
 
         runner.setProperty(PutDatabaseRecord.RECORD_READER_FACTORY, 'parser')
         runner.setProperty(PutDatabaseRecord.STATEMENT_TYPE, 
PutDatabaseRecord.INSERT_TYPE)
@@ -260,22 +273,27 @@ class TestPutDatabaseRecord {
         assertEquals(1, rs.getInt(1))
         assertEquals('rec1', rs.getString(2))
         assertEquals(101, rs.getInt(3))
+        assertEquals(jdbcDate1, rs.getDate(4))
         assertTrue(rs.next())
         assertEquals(2, rs.getInt(1))
         assertEquals('rec2', rs.getString(2))
         assertEquals(102, rs.getInt(3))
+        assertEquals(jdbcDate2, rs.getDate(4))
         assertTrue(rs.next())
         assertEquals(3, rs.getInt(1))
         assertEquals('rec3', rs.getString(2))
         assertEquals(103, rs.getInt(3))
+        assertNull(rs.getDate(4))
         assertTrue(rs.next())
         assertEquals(4, rs.getInt(1))
         assertEquals('rec4', rs.getString(2))
         assertEquals(104, rs.getInt(3))
+        assertNull(rs.getDate(4))
         assertTrue(rs.next())
         assertEquals(5, rs.getInt(1))
         assertNull(rs.getString(2))
         assertEquals(105, rs.getInt(3))
+        assertNull(rs.getDate(4))
         assertFalse(rs.next())
 
         stmt.close()
@@ -633,8 +651,8 @@ class TestPutDatabaseRecord {
         // Set some existing records with different values for name and code
         final Connection conn = dbcp.getConnection()
         Statement stmt = conn.createStatement()
-        stmt.execute('''INSERT INTO PERSONS VALUES (1,'x1',101)''')
-        stmt.execute('''INSERT INTO PERSONS VALUES (2,'x2',102)''')
+        stmt.execute('''INSERT INTO PERSONS VALUES (1,'x1',101, null)''')
+        stmt.execute('''INSERT INTO PERSONS VALUES (2,'x2',102, null)''')
         stmt.close()
 
         runner.enqueue(new byte[0])
@@ -789,9 +807,9 @@ class TestPutDatabaseRecord {
         recreateTable("PERSONS", createPersons)
         Connection conn = dbcp.getConnection()
         Statement stmt = conn.createStatement()
-        stmt.execute("INSERT INTO PERSONS VALUES (1,'rec1', 101)")
-        stmt.execute("INSERT INTO PERSONS VALUES (2,'rec2', 102)")
-        stmt.execute("INSERT INTO PERSONS VALUES (3,'rec3', 103)")
+        stmt.execute("INSERT INTO PERSONS VALUES (1,'rec1', 101, null)")
+        stmt.execute("INSERT INTO PERSONS VALUES (2,'rec2', 102, null)")
+        stmt.execute("INSERT INTO PERSONS VALUES (3,'rec3', 103, null)")
         stmt.close()
 
         final MockRecordParser parser = new MockRecordParser()
@@ -833,9 +851,9 @@ class TestPutDatabaseRecord {
         recreateTable("PERSONS", createPersons)
         Connection conn = dbcp.getConnection()
         Statement stmt = conn.createStatement()
-        stmt.execute("INSERT INTO PERSONS VALUES (1,'rec1', 101)")
-        stmt.execute("INSERT INTO PERSONS VALUES (2,'rec2', null)")
-        stmt.execute("INSERT INTO PERSONS VALUES (3,'rec3', 103)")
+        stmt.execute("INSERT INTO PERSONS VALUES (1,'rec1', 101, null)")
+        stmt.execute("INSERT INTO PERSONS VALUES (2,'rec2', null, null)")
+        stmt.execute("INSERT INTO PERSONS VALUES (3,'rec3', 103, null)")
         stmt.close()
 
         final MockRecordParser parser = new MockRecordParser()

Reply via email to