|
15 | 15 | */ |
16 | 16 | package com.google.cloud.bigquery.storage.v1; |
17 | 17 |
|
| 18 | +import static java.time.temporal.ChronoField.HOUR_OF_DAY; |
| 19 | +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; |
| 20 | +import static java.time.temporal.ChronoField.NANO_OF_SECOND; |
| 21 | +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; |
| 22 | + |
18 | 23 | import com.google.api.pathtemplate.ValidationException; |
19 | 24 | import com.google.cloud.bigquery.storage.v1.Exceptions.RowIndexToErrorException; |
20 | 25 | import com.google.common.annotations.VisibleForTesting; |
@@ -69,7 +74,31 @@ public class JsonToProtoMessage implements ToProtoConverter<Object> { |
69 | 74 | .put(FieldDescriptor.Type.STRING, "string") |
70 | 75 | .put(FieldDescriptor.Type.MESSAGE, "object") |
71 | 76 | .build(); |
72 | | - private static final DateTimeFormatter TIMESTAMP_FORMATTER = |
| 77 | + |
| 78 | + private static final DateTimeFormatter TO_TIMESTAMP_FORMATTER = |
| 79 | + new DateTimeFormatterBuilder() |
| 80 | + .parseLenient() |
| 81 | + .append(DateTimeFormatter.ISO_LOCAL_DATE) |
| 82 | + .optionalStart() |
| 83 | + .appendLiteral('T') |
| 84 | + .optionalEnd() |
| 85 | + .appendValue(HOUR_OF_DAY, 2) |
| 86 | + .appendLiteral(':') |
| 87 | + .appendValue(MINUTE_OF_HOUR, 2) |
| 88 | + .optionalStart() |
| 89 | + .appendLiteral(':') |
| 90 | + .appendValue(SECOND_OF_MINUTE, 2) |
| 91 | + .optionalEnd() |
| 92 | + .optionalStart() |
| 93 | + .appendFraction(NANO_OF_SECOND, 6, 9, true) |
| 94 | + .optionalEnd() |
| 95 | + .optionalStart() |
| 96 | + .appendOffset("+HHMM", "+00:00") |
| 97 | + .optionalEnd() |
| 98 | + .toFormatter() |
| 99 | + .withZone(ZoneOffset.UTC); |
| 100 | + |
| 101 | + private static final DateTimeFormatter FROM_TIMESTAMP_FORMATTER = |
73 | 102 | new DateTimeFormatterBuilder() |
74 | 103 | .parseLenient() |
75 | 104 | .append(DateTimeFormatter.ofPattern("yyyy[/][-]MM[/][-]dd")) |
@@ -634,25 +663,8 @@ private void fillField( |
634 | 663 | return; |
635 | 664 | } |
636 | 665 | } else if (fieldSchema.getType() == TableFieldSchema.Type.TIMESTAMP) { |
637 | | - if (val instanceof String) { |
638 | | - Double parsed = Doubles.tryParse((String) val); |
639 | | - if (parsed != null) { |
640 | | - protoMsg.setField(fieldDescriptor, parsed.longValue()); |
641 | | - return; |
642 | | - } |
643 | | - TemporalAccessor parsedTime = TIMESTAMP_FORMATTER.parse((String) val); |
644 | | - protoMsg.setField( |
645 | | - fieldDescriptor, |
646 | | - parsedTime.getLong(ChronoField.INSTANT_SECONDS) * 1000000 |
647 | | - + parsedTime.getLong(ChronoField.MICRO_OF_SECOND)); |
648 | | - return; |
649 | | - } else if (val instanceof Long) { |
650 | | - protoMsg.setField(fieldDescriptor, val); |
651 | | - return; |
652 | | - } else if (val instanceof Integer) { |
653 | | - protoMsg.setField(fieldDescriptor, Long.valueOf((Integer) val)); |
654 | | - return; |
655 | | - } |
| 666 | + protoMsg.setField(fieldDescriptor, getTimestampAsLong(val)); |
| 667 | + return; |
656 | 668 | } |
657 | 669 | } |
658 | 670 | if (val instanceof Integer) { |
@@ -919,24 +931,7 @@ private void fillRepeatedField( |
919 | 931 | } |
920 | 932 | } else if (fieldSchema != null |
921 | 933 | && fieldSchema.getType() == TableFieldSchema.Type.TIMESTAMP) { |
922 | | - if (val instanceof String) { |
923 | | - Double parsed = Doubles.tryParse((String) val); |
924 | | - if (parsed != null) { |
925 | | - protoMsg.addRepeatedField(fieldDescriptor, parsed.longValue()); |
926 | | - } else { |
927 | | - TemporalAccessor parsedTime = TIMESTAMP_FORMATTER.parse((String) val); |
928 | | - protoMsg.addRepeatedField( |
929 | | - fieldDescriptor, |
930 | | - parsedTime.getLong(ChronoField.INSTANT_SECONDS) * 1000000 |
931 | | - + parsedTime.getLong(ChronoField.MICRO_OF_SECOND)); |
932 | | - } |
933 | | - } else if (val instanceof Long) { |
934 | | - protoMsg.addRepeatedField(fieldDescriptor, val); |
935 | | - } else if (val instanceof Integer) { |
936 | | - protoMsg.addRepeatedField(fieldDescriptor, Long.valueOf((Integer) val)); |
937 | | - } else { |
938 | | - throwWrongFieldType(fieldDescriptor, currentScope, index); |
939 | | - } |
| 934 | + protoMsg.addRepeatedField(fieldDescriptor, getTimestampAsLong(val)); |
940 | 935 | } else if (val instanceof Integer) { |
941 | 936 | protoMsg.addRepeatedField(fieldDescriptor, Long.valueOf((Integer) val)); |
942 | 937 | } else if (val instanceof Long) { |
@@ -1050,18 +1045,51 @@ static Instant fromEpochMicros(long micros) { |
1050 | 1045 | @VisibleForTesting |
1051 | 1046 | static String getTimestampAsString(Object val) { |
1052 | 1047 | if (val instanceof String) { |
1053 | | - // Validate the ISO8601 values before sending it to the server |
1054 | 1048 | String value = (String) val; |
| 1049 | + Double parsed = Doubles.tryParse(value); |
| 1050 | + // If true, it was a numeric value inside a String |
| 1051 | + if (parsed != null) { |
| 1052 | + return getTimestampAsString(parsed.longValue()); |
| 1053 | + } |
| 1054 | + // Validate the ISO8601 values before sending it to the server. No need to format |
| 1055 | + // if it's valid. |
1055 | 1056 | validateTimestamp(value); |
1056 | | - return value; |
1057 | | - } else if (val instanceof Short || val instanceof Integer || val instanceof Long) { |
| 1057 | + |
| 1058 | + // If it's high precision (more than 9 digits), then return the ISO8601 string as-is |
| 1059 | + // as JDK does not have a DateTimeFormatter that supports more than nanosecond precision. |
| 1060 | + Matcher matcher = ISO8601_TIMESTAMP_HIGH_PRECISION_PATTERN.matcher(value); |
| 1061 | + if (matcher.find()) { |
| 1062 | + return value; |
| 1063 | + } |
| 1064 | + // Otherwise, output the timestamp to a standard format before sending it to BQ |
| 1065 | + Instant instant = FROM_TIMESTAMP_FORMATTER.parse(value, Instant::from); |
| 1066 | + return TO_TIMESTAMP_FORMATTER.format(instant); |
| 1067 | + } else if (val instanceof Number) { |
1058 | 1068 | // Micros from epoch will most likely will be represented a Long, but any non-float |
1059 | 1069 | // numeric value can be used |
1060 | | - return fromEpochMicros((Long) val).toString(); |
| 1070 | + Instant instant = fromEpochMicros(((Number) val).longValue()); |
| 1071 | + return TO_TIMESTAMP_FORMATTER.format(instant); |
1061 | 1072 | } else if (val instanceof Timestamp) { |
1062 | 1073 | // Convert the Protobuf timestamp class to ISO8601 string |
1063 | 1074 | Timestamp timestamp = (Timestamp) val; |
1064 | | - return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()).toString(); |
| 1075 | + return TO_TIMESTAMP_FORMATTER.format( |
| 1076 | + Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos())); |
| 1077 | + } |
| 1078 | + throw new IllegalArgumentException("The timestamp value passed in is not from a valid type"); |
| 1079 | + } |
| 1080 | + |
| 1081 | + /* Best effort to try and convert the Object to a long (microseconds since epoch) */ |
| 1082 | + private long getTimestampAsLong(Object val) { |
| 1083 | + if (val instanceof String) { |
| 1084 | + Double parsed = Doubles.tryParse((String) val); |
| 1085 | + if (parsed != null) { |
| 1086 | + return parsed.longValue(); |
| 1087 | + } |
| 1088 | + TemporalAccessor parsedTime = FROM_TIMESTAMP_FORMATTER.parse((String) val); |
| 1089 | + return parsedTime.getLong(ChronoField.INSTANT_SECONDS) * 1000000 |
| 1090 | + + parsedTime.getLong(ChronoField.MICRO_OF_SECOND); |
| 1091 | + } else if (val instanceof Number) { |
| 1092 | + return ((Number) val).longValue(); |
1065 | 1093 | } |
1066 | 1094 | throw new IllegalArgumentException("The timestamp value passed in is not from a valid type"); |
1067 | 1095 | } |
@@ -1107,7 +1135,7 @@ static void validateTimestamp(String timestamp) { |
1107 | 1135 |
|
1108 | 1136 | // It is valid as long as DateTimeFormatter doesn't throw an exception |
1109 | 1137 | try { |
1110 | | - TIMESTAMP_FORMATTER.parse((String) timestamp); |
| 1138 | + FROM_TIMESTAMP_FORMATTER.parse((String) timestamp); |
1111 | 1139 | } catch (DateTimeParseException e) { |
1112 | 1140 | throw new IllegalArgumentException(e.getMessage(), e); |
1113 | 1141 | } |
|
0 commit comments