diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/util/DateUtil.java b/phoenix-core-client/src/main/java/org/apache/phoenix/util/DateUtil.java index 24c79925198..925d33ac117 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/util/DateUtil.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/util/DateUtil.java @@ -220,25 +220,51 @@ public static Time parseTime(String timeValue) { } /** - * Parses the timestsamp string in the UTC time zone. + * Parses the timestamp string in the UTC time zone. * @param timestampValue timestamp string in UTC * @return Timestamp parsed in UTC */ public static Timestamp parseTimestamp(String timestampValue) { - Timestamp timestamp = new Timestamp(parseDateTime(timestampValue)); + return parseTimestamp(timestampValue, null); + } + + /** + * Parses the timestamp string with the given parser. + * @param timestampValue timestamp string + * @param parser Parser with user defined time zone. + * @return Timestamp parsed by the given parser, or parsed in UTC if parser is null. + */ + public static Timestamp parseTimestamp(String timestampValue, DateTimeParser parser) { + Timestamp timestamp; + if (parser == null) { + timestamp = new Timestamp(parseDateTime(timestampValue)); + } else { + timestamp = new Timestamp(parser.parseDateTime(timestampValue)); + } + int nanos = getNanosFromTimestamp(timestampValue); + if (nanos > -1) { + // First clear existing nanos (default is 0), then set the new nanos value + // to avoid accumulating nanos from previous operations + timestamp.setNanos(0); + timestamp.setNanos(nanos); + } + return timestamp; + } + + private static int getNanosFromTimestamp(String timestampValue) { + int nanos = -1; int period = timestampValue.indexOf('.'); if (period > 0) { String nanosStr = timestampValue.substring(period + 1); if (nanosStr.length() > 9) throw new IllegalDataException("nanos > 999999999 or < 0"); if (nanosStr.length() > 3) { - int nanos = Integer.parseInt(nanosStr); + nanos = Integer.parseInt(nanosStr); for (int i = 0; i < 9 - nanosStr.length(); i++) { nanos *= 10; } - timestamp.setNanos(nanos); } } - return timestamp; + return nanos; } /** diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/util/csv/CsvUpsertExecutor.java b/phoenix-core-client/src/main/java/org/apache/phoenix/util/csv/CsvUpsertExecutor.java index 58bd51e4d8c..2bb6e964b89 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/util/csv/CsvUpsertExecutor.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/util/csv/CsvUpsertExecutor.java @@ -162,7 +162,7 @@ public Object apply(@Nullable String input) { return null; } if (dataType == PTimestamp.INSTANCE) { - return DateUtil.parseTimestamp(input); + return DateUtil.parseTimestamp(input, dateTimeParser); } if (dateTimeParser != null) { long epochTime = dateTimeParser.parseDateTime(input); diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/util/json/JsonUpsertExecutor.java b/phoenix-core-client/src/main/java/org/apache/phoenix/util/json/JsonUpsertExecutor.java index 756af4be0ec..61f075b8548 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/util/json/JsonUpsertExecutor.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/util/json/JsonUpsertExecutor.java @@ -187,7 +187,7 @@ public Object apply(@Nullable Object input) { return null; } if (dataType == PTimestamp.INSTANCE) { - return DateUtil.parseTimestamp(input.toString()); + return DateUtil.parseTimestamp(input.toString(), dateTimeParser); } if (dateTimeParser != null && input instanceof String) { final String s = (String) input; diff --git a/phoenix-core/src/test/java/org/apache/phoenix/util/DateUtilTest.java b/phoenix-core/src/test/java/org/apache/phoenix/util/DateUtilTest.java index bcf5111101c..336a4116a91 100644 --- a/phoenix-core/src/test/java/org/apache/phoenix/util/DateUtilTest.java +++ b/phoenix-core/src/test/java/org/apache/phoenix/util/DateUtilTest.java @@ -21,21 +21,38 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; -import java.sql.Date; -import java.sql.Time; -import java.sql.Timestamp; +import java.io.IOException; import java.text.ParseException; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; import java.util.TimeZone; + +import org.apache.phoenix.query.QueryServices; +import org.apache.phoenix.util.ColumnInfo; +import org.apache.phoenix.util.DateUtil; import org.apache.phoenix.schema.IllegalDataException; import org.apache.phoenix.schema.types.PDate; +import org.apache.phoenix.schema.types.PInteger; +import org.apache.phoenix.schema.types.PIntegerArray; import org.apache.phoenix.schema.types.PTime; import org.apache.phoenix.schema.types.PTimestamp; import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import org.apache.phoenix.thirdparty.com.google.common.base.Joiner; +import org.apache.phoenix.thirdparty.com.google.common.collect.Iterables; /** * Test class for {@link DateUtil} @@ -352,4 +369,49 @@ public void testTZCorrection() { assertEquals(DateUtil.applyInputDisplacement(startOfWinterLocal, tz), startOfWinterDisplaced); assertEquals(DateUtil.applyOutputDisplacement(startOfWinterDisplaced, tz), startOfWinterLocal); } + + /** + * parseTimestamp should use the configured timezone from parser instead of hardcoded UTC. + * Test logic: + * 1. Create a DateTimeParser with Asia/Shanghai timezone + * 2. Parse timestamp string "2020-01-01 00:00:00.000" using the parser + * 3. Verify parsed epoch millis is 8 hours less than UTC parsed result + */ + @Test + public void testParseTimestampWithCustomTimeZone() throws ParseException { + String tsStr = "2020-01-01 00:00:00.000"; + java.util.TimeZone shanghaiZone = java.util.TimeZone.getTimeZone("Asia/Shanghai"); + + java.sql.Timestamp parsedTs = DateUtil.parseTimestamp(tsStr, + DateUtil.getDateTimeParser("yyyy-MM-dd HH:mm:ss.SSS", PTimestamp.INSTANCE, shanghaiZone.getID())); + + // DateUtil.parseTimestamp() parses using UTC by default when no parser is provided + java.sql.Timestamp utcTs = DateUtil.parseTimestamp(tsStr); + + // Asia/Shanghai is UTC+8, so the parsed epoch time should be 8 hours less than UTC result + long expectedMillis = utcTs.getTime() - (8L * 60 * 60 * 1000); + + assertEquals("Timestamp should be parsed using configured timezone Asia/Shanghai", expectedMillis, parsedTs.getTime()); + } + + /** + * Verifies that nanosecond precision is correctly preserved under a custom timezone + */ + @Test + public void testParseTimestampWithCustomTimeZoneAndNanos() throws ParseException { + String tsStr = "2020-01-01 00:00:00.123"; // Only 3 nanoseconds + java.util.TimeZone shanghaiZone = java.util.TimeZone.getTimeZone("Asia/Shanghai"); + + java.sql.Timestamp parsedTs = DateUtil.parseTimestamp(tsStr, + DateUtil.getDateTimeParser("yyyy-MM-dd HH:mm:ss.SSS", PTimestamp.INSTANCE, shanghaiZone.getID())); + + // DateUtil.parseTimestamp() parses using UTC by default when no parser is provided + java.sql.Timestamp utcTs = DateUtil.parseTimestamp(tsStr); + + // Asia/Shanghai is UTC+8, so the parsed epoch time should be 8 hours less than UTC result + long expectedMillis = utcTs.getTime() - (8L * 60 * 60 * 1000); + + assertEquals("Epoch millis should reflect Asia/Shanghai timezone", expectedMillis, parsedTs.getTime()); + assertEquals("Nanos should be preserved correctly", 123000000, parsedTs.getNanos()); + } }