Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.beam.sdk.extensions.sql.zetasql; import static com.google.zetasql.CivilTimeEncoder.decodePacked64TimeNanos; import static com.google.zetasql.CivilTimeEncoder.encodePacked64TimeNanos; import com.google.zetasql.Value; import io.grpc.Status; import java.util.List; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Splitter; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableMap; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Lists; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.math.LongMath; import org.apache.calcite.avatica.util.TimeUnit; import org.apache.calcite.util.DateString; import org.apache.calcite.util.TimeString; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.LocalTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; /** DateTimeUtils. */ public class DateTimeUtils { public static final Long MILLIS_PER_DAY = 86400000L; private static final Long MICROS_PER_MILLI = 1000L; @SuppressWarnings("unchecked") private enum TimestampPatterns { TIMESTAMP_PATTERN, TIMESTAMP_PATTERN_SUBSECOND, TIMESTAMP_PATTERN_T, TIMESTAMP_PATTERN_SUBSECOND_T, } @SuppressWarnings("unchecked") private static final ImmutableMap<Enum, DateTimeFormatter> TIMESTAMP_PATTERN_WITHOUT_TZ = ImmutableMap.of( TimestampPatterns.TIMESTAMP_PATTERN, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"), TimestampPatterns.TIMESTAMP_PATTERN_SUBSECOND, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS"), TimestampPatterns.TIMESTAMP_PATTERN_T, DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss"), TimestampPatterns.TIMESTAMP_PATTERN_SUBSECOND_T, DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")); @SuppressWarnings("unchecked") private static final ImmutableMap<Enum, DateTimeFormatter> TIMESTAMP_PATTERN_WITH_TZ = ImmutableMap.of( TimestampPatterns.TIMESTAMP_PATTERN, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ssZZ"), TimestampPatterns.TIMESTAMP_PATTERN_SUBSECOND, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSZZ"), TimestampPatterns.TIMESTAMP_PATTERN_T, DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZZ"), TimestampPatterns.TIMESTAMP_PATTERN_SUBSECOND_T, DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZ")); public static DateTimeFormatter findDateTimePattern(String str) { if (str.indexOf('+') == -1) { return findDateTimePattern(str, TIMESTAMP_PATTERN_WITHOUT_TZ); } else { return findDateTimePattern(str, TIMESTAMP_PATTERN_WITH_TZ); } } @SuppressWarnings("unchecked") public static DateTimeFormatter findDateTimePattern(String str, ImmutableMap<Enum, DateTimeFormatter> patternMap) { if (str.indexOf('.') == -1) { if (str.indexOf('T') == -1) { return patternMap.get(TimestampPatterns.TIMESTAMP_PATTERN); } else { return patternMap.get(TimestampPatterns.TIMESTAMP_PATTERN_T); } } else { if (str.indexOf('T') == -1) { return patternMap.get(TimestampPatterns.TIMESTAMP_PATTERN_SUBSECOND); } else { return patternMap.get(TimestampPatterns.TIMESTAMP_PATTERN_SUBSECOND_T); } } } // https://cloud.google.com/bigquery/docs/reference/standard-sql/migrating-from-legacy-sql#timestamp_differences // 0001-01-01 00:00:00 to 9999-12-31 23:59:59.999999 UTC. // -62135596800000000 to 253402300799999999 @SuppressWarnings("GoodTime") public static final Long MIN_UNIX_MILLIS = -62135596800000L; @SuppressWarnings("GoodTime") public static final Long MAX_UNIX_MILLIS = 253402300799999L; public static DateTime parseTimestampWithUTCTimeZone(String str) { return findDateTimePattern(str).withZoneUTC().parseDateTime(str); } @SuppressWarnings("unused") public static DateTime parseTimestampWithLocalTimeZone(String str) { return findDateTimePattern(str).withZone(DateTimeZone.getDefault()).parseDateTime(str); } public static DateTime parseTimestampWithTimeZone(String str) { // for example, accept "1990-10-20 13:24:01+0730" if (str.indexOf('.') == -1) { return DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ssZ").parseDateTime(str); } else { return DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSZ").parseDateTime(str); } } public static String formatTimestampWithTimeZone(DateTime dt) { String resultWithoutZone; if (dt.getMillisOfSecond() == 0) { resultWithoutZone = dt.toString(DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss")); } else { resultWithoutZone = dt.toString(DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS")); } // ZetaSQL expects a 2-digit timezone offset (-05) if the minute part is zero, and it expects // a 4-digit timezone with a colon (-07:52) if the minute part is non-zero. None of the // variations on z,Z,ZZ,.. do this for us so we have to do it manually here. String zone = dt.toString(DateTimeFormat.forPattern("ZZ")); List<String> zoneParts = Lists.newArrayList(Splitter.on(':').limit(2).split(zone)); if (zoneParts.size() == 2 && zoneParts.get(1).equals("00")) { zone = zoneParts.get(0); } return resultWithoutZone + zone; } @SuppressWarnings("unused") public static DateTime parseTimestampWithoutTimeZone(String str) { return DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss").parseDateTime(str); } public static DateTime parseDate(String str) { return DateTimeFormat.forPattern("yyyy-MM-dd").withZoneUTC().parseDateTime(str); } public static DateTime parseTime(String str) { // DateTimeFormat does not parse "08:10:10" for pattern "HH:mm:ss.SSS". In this case, '.' must // appear. if (str.indexOf('.') == -1) { return DateTimeFormat.forPattern("HH:mm:ss").withZoneUTC().parseDateTime(str); } else { return DateTimeFormat.forPattern("HH:mm:ss.SSS").withZoneUTC().parseDateTime(str); } } @SuppressWarnings("Value with nanoseconds will be truncated to milliseconds in decodePacked64TimeNanos.") public static TimeString convertTimeValueToTimeString(Value value) { LocalTime localTime = decodePacked64TimeNanos(value.getTimeValue()); return TimeString.fromMillisOfDay(localTime.getMillisOfDay()); } // dates are represented as an int32 value, indicating the offset // in days from the epoch 1970-01-01. ZetaSQL dates are not timezone aware, // and do not correspond to any particular 24 hour period. public static DateString convertDateValueToDateString(Value value) { return DateString.fromDaysSinceEpoch(value.getDateValue()); } public static Value parseDateToValue(String dateString) { DateTime dateTime = parseDate(dateString); return Value.createDateValue((int) (dateTime.getMillis() / MILLIS_PER_DAY)); } public static Value parseTimeToValue(String timeString) { DateTime dateTime = parseTime(timeString); return Value.createTimeValue(encodePacked64TimeNanos(LocalTime.fromMillisOfDay(dateTime.getMillisOfDay()))); } public static Value parseTimestampWithTZToValue(String timestampString) { DateTime dateTime = parseTimestampWithTimeZone(timestampString); // convert from micros. // TODO: how to handle overflow. return Value.createTimestampValueFromUnixMicros( LongMath.checkedMultiply(dateTime.getMillis(), MICROS_PER_MILLI)); } private static void safeCheckSubMillisPrecision(long micros) { long subMilliPrecision = micros % 1000L; if (subMilliPrecision != 0) { throw new IllegalArgumentException(String.format( "%s has sub-millisecond precision, which Beam ZetaSQL does" + " not currently support.", micros)); } } @SuppressWarnings("GoodTime") public static long safeMicrosToMillis(long micros) { safeCheckSubMillisPrecision(micros); return micros / 1000L; } /** * This function validates that Long representation of timestamp is compatible with ZetaSQL * timestamp values range. * * <p>Invoked via reflection. @see SqlOperators * * @param ts Timestamp to validate. * @return Unchanged timestamp sent for validation. */ @SuppressWarnings("GoodTime") public static Long validateTimestamp(Long ts) { if (ts == null) { return null; } if ((ts < MIN_UNIX_MILLIS) || (ts > MAX_UNIX_MILLIS)) { throw Status.OUT_OF_RANGE.withDescription("Timestamp is out of valid range.").asRuntimeException(); } return ts; } /** * This function validates that interval is compatible with ZetaSQL timestamp values range. * * <p>ZetaSQL validates that if we represent interval in milliseconds, it will fit into Long. * * <p>In case of SECOND or smaller time unit, it converts timestamp to microseconds, so we need to * convert those to microsecond and verify that we do not cause overflow. * * <p>Invoked via reflection. @see SqlOperators * * @param arg Argument for the interval. * @param unit Time unit used in this interval. * @return Argument for the interval. */ @SuppressWarnings("GoodTime") public static Long validateTimeInterval(Long arg, TimeUnit unit) { if (arg == null) { return null; } // multiplier to convert to milli or microseconds. long multiplier = unit.multiplier.longValue(); switch (unit) { case SECOND: case MILLISECOND: multiplier *= 1000L; // Change multiplier from milliseconds to microseconds. break; default: break; } if ((arg > Long.MAX_VALUE / multiplier) || (arg < Long.MIN_VALUE / multiplier)) { throw Status.OUT_OF_RANGE.withDescription("Interval is out of valid range").asRuntimeException(); } return arg; } }