From df6ebe79aced22d611a1ecd2ba63d1094fa0fc68 Mon Sep 17 00:00:00 2001 From: Sagar Agarwal Date: Wed, 16 Oct 2024 16:59:53 +0530 Subject: [PATCH] Add support for Interval in Java Client --- .../clirr-ignored-differences.xml | 52 +++ google-cloud-spanner/pom.xml | 11 + .../cloud/spanner/AbstractResultSet.java | 10 + .../cloud/spanner/AbstractStructReader.java | 30 ++ .../cloud/spanner/ForwardingStructReader.java | 24 ++ .../com/google/cloud/spanner/GrpcStruct.java | 26 ++ .../com/google/cloud/spanner/Interval.java | 320 ++++++++++++++++++ .../com/google/cloud/spanner/ResultSets.java | 20 ++ .../java/com/google/cloud/spanner/Struct.java | 14 + .../google/cloud/spanner/StructReader.java | 28 ++ .../java/com/google/cloud/spanner/Type.java | 17 + .../java/com/google/cloud/spanner/Value.java | 94 +++++ .../com/google/cloud/spanner/ValueBinder.java | 10 + .../connection/DirectExecuteResultSet.java | 31 +- .../ReplaceableForwardingResultSet.java | 33 +- .../AbstractStructReaderTypesTest.java | 27 ++ .../cloud/spanner/GrpcResultSetTest.java | 46 +++ .../google/cloud/spanner/IntervalTest.java | 162 +++++++++ .../cloud/spanner/MockSpannerServiceImpl.java | 17 + .../spanner/RandomResultSetGenerator.java | 15 + .../google/cloud/spanner/ResultSetsTest.java | 25 ++ .../com/google/cloud/spanner/TypeTest.java | 26 ++ .../google/cloud/spanner/ValueBinderTest.java | 39 +++ .../com/google/cloud/spanner/ValueTest.java | 94 +++++ .../connection/ChecksumResultSetTest.java | 33 +- .../cloud/spanner/it/ITIntervalTest.java | 214 ++++++++++++ .../google/cloud/spanner/it/ITQueryTest.java | 67 +++- 27 files changed, 1449 insertions(+), 36 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/Interval.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntervalTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITIntervalTest.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index ec13415790..e121fce7ab 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -566,6 +566,58 @@ java.util.List getFloat32Array() + + + 7012 + com/google/cloud/spanner/StructReader + com.google.cloud.spanner.Interval getInterval(int) + + + 7012 + com/google/cloud/spanner/StructReader + com.google.cloud.spanner.Interval getInterval(java.lang.String) + + + 7012 + com/google/cloud/spanner/StructReader + com.google.cloud.spanner.Interval[] getIntervalArray(int) + + + 7012 + com/google/cloud/spanner/StructReader + com.google.cloud.spanner.Interval[] getIntervalArray(java.lang.String) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getIntervalList(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getIntervalList(java.lang.String) + + + 7013 + com/google/cloud/spanner/AbstractStructReader + com.google.cloud.spanner.Interval getIntervalInternal(int) + + + 7013 + com/google/cloud/spanner/AbstractStructReader + java.util.List getIntervalListInternal(int) + + + 7013 + com/google/cloud/spanner/Value + com.google.cloud.spanner.Interval getInterval() + + + 7013 + com/google/cloud/spanner/Value + java.util.List getIntervalArray() + + 7012 diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 9719e08176..1f33ef2d82 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -364,6 +364,11 @@ ${graal-sdk.version} provided + + com.google.auto.value + auto-value + 1.10.4 + @@ -455,6 +460,12 @@ opentelemetry-sdk-testing test + + org.jetbrains + annotations + 24.1.0 + compile + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java index 2cf93fb92e..2d4088e486 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java @@ -415,6 +415,11 @@ protected Date getDateInternal(int columnIndex) { return currRow().getDateInternal(columnIndex); } + @Override + protected Interval getIntervalInternal(int columnIndex) { + return currRow().getIntervalInternal(columnIndex); + } + @Override protected Value getValueInternal(int columnIndex) { return currRow().getValueInternal(columnIndex); @@ -507,6 +512,11 @@ protected List getDateListInternal(int columnIndex) { return currRow().getDateListInternal(columnIndex); } + @Override + protected List getIntervalListInternal(int columnIndex) { + return currRow().getIntervalListInternal(columnIndex); + } + @Override protected List getStructListInternal(int columnIndex) { return currRow().getStructListInternal(columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java index d13c61aaf0..10287544e9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java @@ -67,6 +67,8 @@ protected String getPgJsonbInternal(int columnIndex) { protected abstract Date getDateInternal(int columnIndex); + protected abstract Interval getIntervalInternal(int columnIndex); + protected T getProtoMessageInternal(int columnIndex, T message) { throw new UnsupportedOperationException("Not implemented"); } @@ -128,6 +130,8 @@ protected List getPgJsonbListInternal(int columnIndex) { protected abstract List getDateListInternal(int columnIndex); + protected abstract List getIntervalListInternal(int columnIndex); + protected abstract List getStructListInternal(int columnIndex); @Override @@ -299,6 +303,19 @@ public Date getDate(String columnName) { return getDateInternal(columnIndex); } + @Override + public Interval getInterval(int columnIndex) { + checkNonNullOfType(columnIndex, Type.interval(), columnIndex); + return getIntervalInternal(columnIndex); + } + + @Override + public Interval getInterval(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.interval(), columnName); + return getIntervalInternal(columnIndex); + } + @Override public T getProtoEnum( int columnIndex, Function method) { @@ -583,6 +600,19 @@ public List getDateList(String columnName) { return getDateListInternal(columnIndex); } + @Override + public List getIntervalList(int columnIndex) { + checkNonNullOfType(columnIndex, Type.array(Type.interval()), columnIndex); + return getIntervalListInternal(columnIndex); + } + + @Override + public List getIntervalList(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.array(Type.interval()), columnName); + return getIntervalListInternal(columnIndex); + } + @Override public List getStructList(int columnIndex) { checkNonNullArrayOfStruct(columnIndex, columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java index b3e37ffcdd..c747c2a590 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java @@ -231,6 +231,18 @@ public Date getDate(String columnName) { return delegate.get().getDate(columnName); } + @Override + public Interval getInterval(int columnIndex) { + checkValidState(); + return delegate.get().getInterval(columnIndex); + } + + @Override + public Interval getInterval(String columnName) { + checkValidState(); + return delegate.get().getInterval(columnName); + } + @Override public boolean[] getBooleanArray(int columnIndex) { checkValidState(); @@ -409,6 +421,18 @@ public List getDateList(String columnName) { return delegate.get().getDateList(columnName); } + @Override + public List getIntervalList(int columnIndex) { + checkValidState(); + return delegate.get().getIntervalList(columnIndex); + } + + @Override + public List getIntervalList(String columnName) { + checkValidState(); + return delegate.get().getIntervalList(columnName); + } + @Override public List getProtoMessageList(int columnIndex, T message) { checkValidState(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java index 4d07a12880..ef4643bb63 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java @@ -131,6 +131,9 @@ private Object writeReplace() { case DATE: builder.set(fieldName).to((Date) value); break; + case INTERVAL: + builder.set(fieldName).to((Interval) value); + break; case ARRAY: final Type elementType = fieldType.getArrayElementType(); switch (elementType.getCode()) { @@ -184,6 +187,9 @@ private Object writeReplace() { case DATE: builder.set(fieldName).toDateArray((Iterable) value); break; + case INTERVAL: + builder.set(fieldName).toIntervalArray((Iterable) value); + break; case STRUCT: builder.set(fieldName).toStructArray(elementType, (Iterable) value); break; @@ -298,6 +304,9 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot case DATE: checkType(fieldType, proto, KindCase.STRING_VALUE); return Date.parseDate(proto.getStringValue()); + case INTERVAL: + checkType(fieldType, proto, KindCase.STRING_VALUE); + return Interval.parseFromString(proto.getStringValue()); case ARRAY: checkType(fieldType, proto, KindCase.LIST_VALUE); ListValue listValue = proto.getListValue(); @@ -347,6 +356,7 @@ static Object decodeArrayValue(Type elementType, ListValue listValue) { case BYTES: case TIMESTAMP: case DATE: + case INTERVAL: case STRUCT: case PROTO: return Lists.transform(listValue.getValuesList(), input -> decodeValue(elementType, input)); @@ -503,6 +513,11 @@ protected Date getDateInternal(int columnIndex) { return (Date) rowData.get(columnIndex); } + protected Interval getIntervalInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Interval) rowData.get(columnIndex); + } + private boolean isUnrecognizedType(int columnIndex) { return type.getStructFields().get(columnIndex).getType().getCode() == Code.UNRECOGNIZED; } @@ -624,6 +639,8 @@ protected Value getValueInternal(int columnIndex) { return Value.timestamp(isNull ? null : getTimestampInternal(columnIndex)); case DATE: return Value.date(isNull ? null : getDateInternal(columnIndex)); + case INTERVAL: + return Value.interval(isNull ? null : getIntervalInternal(columnIndex)); case STRUCT: return Value.struct(isNull ? null : getStructInternal(columnIndex)); case UNRECOGNIZED: @@ -664,6 +681,8 @@ protected Value getValueInternal(int columnIndex) { return Value.timestampArray(isNull ? null : getTimestampListInternal(columnIndex)); case DATE: return Value.dateArray(isNull ? null : getDateListInternal(columnIndex)); + case INTERVAL: + return Value.intervalArray(isNull ? null : getIntervalListInternal(columnIndex)); case STRUCT: return Value.structArray( elementType, isNull ? null : getStructListInternal(columnIndex)); @@ -847,6 +866,13 @@ protected List getDateListInternal(int columnIndex) { return Collections.unmodifiableList((List) rowData.get(columnIndex)); } + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getIntervalListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + @Override @SuppressWarnings("unchecked") // We know ARRAY> produces a List. protected List getStructListInternal(int columnIndex) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Interval.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Interval.java new file mode 100644 index 0000000000..b9acd60535 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Interval.java @@ -0,0 +1,320 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed 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 com.google.cloud.spanner; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jetbrains.annotations.NotNull; + +@AutoValue +@Immutable +public abstract class Interval implements Comparable, Serializable { + public static final long MONTHS_PER_YEAR = 12; + public static final long DAYS_PER_MONTH = 30; + public static final long HOURS_PER_DAY = 24; + public static final long MINUTES_PER_HOUR = 60; + public static final long SECONDS_PER_MINUTE = 60; + public static final long SECONDS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE; + public static final long MILLIS_PER_SECOND = 1000; + public static final long MICROS_PER_MILLI = 1000; + public static final long NANOS_PER_MICRO = 1000; + public static final long MICROS_PER_SECOND = MICROS_PER_MILLI * MILLIS_PER_SECOND; + public static final long MICROS_PER_MINUTE = SECONDS_PER_MINUTE * MICROS_PER_SECOND; + public static final long MICROS_PER_HOUR = SECONDS_PER_HOUR * MICROS_PER_SECOND; + public static final long MICROS_PER_DAY = HOURS_PER_DAY * MICROS_PER_HOUR; + public static final long MICROS_PER_MONTH = DAYS_PER_MONTH * MICROS_PER_DAY; + public static final BigInteger NANOS_PER_SECOND = + BigInteger.valueOf(MICROS_PER_SECOND * NANOS_PER_MICRO); + public static final BigInteger NANOS_PER_MINUTE = + BigInteger.valueOf(MICROS_PER_MINUTE * NANOS_PER_MICRO); + public static final BigInteger NANOS_PER_HOUR = + BigInteger.valueOf(MICROS_PER_HOUR * NANOS_PER_MICRO); + + private static final Pattern INTERVAL_PATTERN = + Pattern.compile( + "^P(?!$)(-?\\d+Y)?(-?\\d+M)?(-?\\d+D)?(T(?=-?\\d)(-?\\d+H)?(-?\\d+M)?(-?\\d+(\\.\\d{1,9})?S)?)?$"); + + public abstract long months(); + + public abstract long days(); + + public abstract long micros(); + + public abstract short nanoFractions(); + + public static Builder builder() { + return new AutoValue_Interval.Builder(); + } + + public BigInteger nanos() { + return BigInteger.valueOf(micros()) + .multiply(BigInteger.valueOf(NANOS_PER_MICRO)) + .add(BigInteger.valueOf(nanoFractions())); + } + + /** Returns the total micros represented by the Interval. */ + public long getAsMicros() { + return months() * MICROS_PER_MONTH + days() * MICROS_PER_DAY + micros(); + } + + /** Returns the total nanos represented by the Interval. */ + public BigInteger getAsNanos() { + return BigInteger.valueOf(getAsMicros()) + .multiply(BigInteger.valueOf(NANOS_PER_MICRO)) + .add(BigInteger.valueOf(nanoFractions())); + } + + /** Creates an Interval consisting of the given number of months. */ + public static Interval ofMonths(long months) { + return builder().setMonths(months).setDays(0).setMicros(0).setNanoFractions((short) 0).build(); + } + + /** Creates an Interval consisting of the given number of days. */ + public static Interval ofDays(long days) { + return builder().setMonths(0).setDays(days).setMicros(0).setNanoFractions((short) 0).build(); + } + + /** Creates an Interval with specified months, days and micros. */ + public static Interval fromMonthsDaysMicros(long months, long days, long micros) { + return builder() + .setMonths(months) + .setDays(days) + .setMicros(micros) + .setNanoFractions((short) 0) + .build(); + } + + /** Creates an Interval with specified months, days and nanos. */ + public static Interval fromMonthsDaysNanos(long months, long days, BigInteger nanos) { + long micros = nanos.divide(BigInteger.valueOf(NANOS_PER_MICRO)).longValue(); + short nanoFractions = + nanos + .subtract(BigInteger.valueOf(micros).multiply(BigInteger.valueOf(NANOS_PER_MICRO))) + .shortValue(); + return builder() + .setMonths(months) + .setDays(days) + .setMicros(micros) + .setNanoFractions(nanoFractions) + .build(); + } + + private static String getNullOrDefault(Matcher matcher, int groupIdx) { + String value = matcher.group(groupIdx); + return value == null ? "0" : value; + } + + public static Interval parseFromString(String interval) { + Matcher matcher = INTERVAL_PATTERN.matcher(interval); + if (!matcher.matches()) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, "Invalid Interval String: " + interval); + } + + long years = Long.parseLong(getNullOrDefault(matcher, 1).replace("Y", "")); + long months = Long.parseLong(getNullOrDefault(matcher, 2).replace("M", "")); + long days = Long.parseLong(getNullOrDefault(matcher, 3).replace("D", "")); + long hours = Long.parseLong(getNullOrDefault(matcher, 5).replace("H", "")); + long minutes = Long.parseLong(getNullOrDefault(matcher, 6).replace("M", "")); + BigDecimal seconds = new BigDecimal(getNullOrDefault(matcher, 7).replace("S", "")); + + long totalMonths = years * MONTHS_PER_YEAR + months; + BigInteger totalNanos = seconds.movePointRight(9).toBigInteger(); + totalNanos = + totalNanos.add(BigInteger.valueOf(minutes * SECONDS_PER_MINUTE).multiply(NANOS_PER_SECOND)); + totalNanos = + totalNanos.add(BigInteger.valueOf(hours * SECONDS_PER_HOUR).multiply(NANOS_PER_SECOND)); + + BigInteger totalMicros = totalNanos.divide(BigInteger.valueOf(NANOS_PER_MICRO)); + BigInteger nanoFractions = + totalNanos.subtract(totalMicros.multiply(BigInteger.valueOf(NANOS_PER_MICRO))); + + return Interval.builder() + .setMonths(totalMonths) + .setDays(days) + .setMicros(totalMicros.longValue()) + .setNanoFractions(nanoFractions.shortValue()) + .build(); + } + + /** @return the Interval in ISO801 duration format. */ + public String ToISO8601() { + StringBuilder result = new StringBuilder(); + result.append("P"); + + long months = this.months(); + long years = months / MONTHS_PER_YEAR; + months = months - years * MONTHS_PER_YEAR; + + if (years != 0) { + result.append(String.format("%dY", years)); + } + + if (months != 0) { + result.append(String.format("%dM", months)); + } + + if (this.days() != 0) { + result.append(String.format("%dD", this.days())); + } + + BigInteger nanos = this.nanos(); + BigInteger zero = BigInteger.valueOf(0); + if (nanos.compareTo(zero) != 0) { + result.append("T"); + BigInteger hours = nanos.divide(NANOS_PER_HOUR); + + if (hours.compareTo(zero) != 0) { + result.append(String.format("%sH", hours)); + } + + nanos = nanos.subtract(hours.multiply(NANOS_PER_HOUR)); + BigInteger minutes = nanos.divide(NANOS_PER_MINUTE); + if (minutes.compareTo(zero) != 0) { + result.append(String.format("%sM", minutes)); + } + + nanos = nanos.subtract(minutes.multiply(NANOS_PER_MINUTE)); + BigDecimal seconds = new BigDecimal(nanos).movePointLeft(9); + + if (seconds.compareTo(new BigDecimal(zero)) != 0) { + result.append(String.format("%sS", seconds)); + } + } + + if (result.length() == 1) { + result.append("0Y"); + } + + return result.toString(); + } + + /** Creates an Interval consisting of the given number of seconds. */ + public static Interval ofSeconds(long seconds) { + return builder() + .setMonths(0) + .setDays(0) + .setMicros(seconds * MICROS_PER_SECOND) + .setNanoFractions((short) 0) + .build(); + } + + /** Creates an Interval consisting of the given number of milliseconds. */ + public static Interval ofMilliseconds(long milliseconds) { + return builder() + .setMonths(0) + .setDays(0) + .setMicros(milliseconds * MICROS_PER_MILLI) + .setNanoFractions((short) 0) + .build(); + } + + /** Creates an Interval consisting of the given number of microseconds. */ + public static Interval ofMicros(long micros) { + return builder().months(0).days(0).micros(micros).nanoFractions((short) 0).build(); + } + + /** Creates an Interval consisting of the given number of nanoseconds. */ + public static Interval ofNanos(@NotNull BigInteger nanos) { + BigInteger micros = nanos.divide(BigInteger.valueOf(NANOS_PER_MICRO)); + BigInteger nanoFractions = nanos.subtract(micros.multiply(BigInteger.valueOf(NANOS_PER_MICRO))); + + long microsValue = micros.longValue(); + long nanoFractionsValue = nanoFractions.longValue(); + + return builder() + .setMonths(0) + .setDays(0) + .setMicros(microsValue) + .setNanoFractions((short) nanoFractionsValue) + .build(); + } + + public static Interval zeroInterval() { + return builder().setMonths(0).setDays(0).setMicros(0).setNanoFractions((short) 0).build(); + } + + @Override + public boolean equals(Object rhs) { + if (!(rhs instanceof Interval)) { + return false; + } + + Interval anotherInterval = (Interval) rhs; + return months() == anotherInterval.months() + && days() == anotherInterval.days() + && nanos().equals(anotherInterval.nanos()); + } + + @Override + public int compareTo(@NotNull Interval anotherInterval) { + if (equals(anotherInterval)) { + return 0; + } + return getAsNanos().compareTo(anotherInterval.getAsNanos()); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + Long.valueOf(months()).hashCode(); + result = 31 * result + Long.valueOf(days()).hashCode(); + result = 31 * result + nanos().hashCode(); + return result; + } + + @AutoValue.Builder + public abstract static class Builder { + abstract Builder months(long months); + + abstract Builder days(long days); + + abstract Builder micros(long micros); + + abstract Builder nanoFractions(short nanoFractions); + + public Builder setMonths(long months) { + return months(months); + } + + public Builder setDays(long days) { + return days(days); + } + + public Builder setMicros(long micros) { + return micros(micros); + } + + public Builder setNanoFractions(short nanoFractions) { + if (nanoFractions <= -NANOS_PER_MICRO || nanoFractions >= NANOS_PER_MICRO) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + String.format( + "NanoFractions must be between:[-%d, %d]", + NANOS_PER_MICRO - 1, NANOS_PER_MICRO - 1)); + } + return nanoFractions(nanoFractions); + } + + public abstract Interval build(); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java index 3d12cf5ad2..d91bc2104a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java @@ -326,6 +326,16 @@ public Date getDate(String columnName) { return getCurrentRowAsStruct().getDate(columnName); } + @Override + public Interval getInterval(int columnIndex) { + return getCurrentRowAsStruct().getInterval(columnIndex); + } + + @Override + public Interval getInterval(String columnName) { + return getCurrentRowAsStruct().getInterval(columnName); + } + @Override public T getProtoMessage(int columnIndex, T message) { return getCurrentRowAsStruct().getProtoMessage(columnIndex, message); @@ -508,6 +518,16 @@ public List getDateList(String columnName) { return getCurrentRowAsStruct().getDateList(columnName); } + @Override + public List getIntervalList(int columnIndex) { + return getCurrentRowAsStruct().getIntervalList(columnIndex); + } + + @Override + public List getIntervalList(String columnName) { + return getCurrentRowAsStruct().getIntervalList(columnName); + } + @Override public List getProtoMessageList(int columnIndex, T message) { return getCurrentRowAsStruct().getProtoMessageList(columnIndex, message); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java index 112ecc8120..7944c7f409 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java @@ -226,6 +226,11 @@ protected Date getDateInternal(int columnIndex) { return values.get(columnIndex).getDate(); } + @Override + protected Interval getIntervalInternal(int columnIndex) { + return values.get(columnIndex).getInterval(); + } + @Override protected T getProtoMessageInternal(int columnIndex, T message) { return values.get(columnIndex).getProtoMessage(message); @@ -334,6 +339,11 @@ protected List getDateListInternal(int columnIndex) { return values.get(columnIndex).getDateArray(); } + @Override + protected List getIntervalListInternal(int columnIndex) { + return values.get(columnIndex).getIntervalArray(); + } + @Override protected List getStructListInternal(int columnIndex) { return values.get(columnIndex).getStructArray(); @@ -420,6 +430,8 @@ private Object getAsObject(int columnIndex) { return getTimestampInternal(columnIndex); case DATE: return getDateInternal(columnIndex); + case INTERVAL: + return getIntervalInternal(columnIndex); case STRUCT: return getStructInternal(columnIndex); case ARRAY: @@ -451,6 +463,8 @@ private Object getAsObject(int columnIndex) { return getTimestampListInternal(columnIndex); case DATE: return getDateListInternal(columnIndex); + case INTERVAL: + return getIntervalListInternal(columnIndex); case STRUCT: return getStructListInternal(columnIndex); default: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java index f9967db045..33e88c39d3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java @@ -297,6 +297,18 @@ default T getProtoEnum( */ Date getDate(String columnName); + /** + * @param columnIndex index of the column + * @return the value of a non-{@code NULL} column with type {@link Type#interval()}. + */ + Interval getInterval(int columnIndex); + + /** + * @param columnName name of the column + * @return the value of a non-{@code NULL} column with type {@link Type#interval()}. + */ + Interval getInterval(String columnName); + /** * @param columnIndex index of the column * @return the value of a nullable column as a {@link Value}. @@ -625,6 +637,22 @@ default List getProtoEnumList( */ List getDateList(String columnName); + /** + * @param columnIndex index of the column + * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.interval())}. + * The list returned by this method is lazily constructed. Create a copy of it if you intend + * to access each element in the list multiple times. + */ + List getIntervalList(int columnIndex); + + /** + * @param columnName name of the column + * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.interval())}. + * The list returned by this method is lazily constructed. Create a copy of it if you intend + * to access each element in the list multiple times. + */ + List getIntervalList(String columnName); + /** * @param columnIndex index of the column * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.struct(...))} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java index 748cb7f87e..7cb3f131f7 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java @@ -59,6 +59,7 @@ public final class Type implements Serializable { private static final Type TYPE_BYTES = new Type(Code.BYTES, null, null); private static final Type TYPE_TIMESTAMP = new Type(Code.TIMESTAMP, null, null); private static final Type TYPE_DATE = new Type(Code.DATE, null, null); + private static final Type TYPE_INTERVAL = new Type(Code.INTERVAL, null, null); private static final Type TYPE_ARRAY_BOOL = new Type(Code.ARRAY, TYPE_BOOL, null); private static final Type TYPE_ARRAY_INT64 = new Type(Code.ARRAY, TYPE_INT64, null); private static final Type TYPE_ARRAY_FLOAT32 = new Type(Code.ARRAY, TYPE_FLOAT32, null); @@ -72,6 +73,7 @@ public final class Type implements Serializable { private static final Type TYPE_ARRAY_BYTES = new Type(Code.ARRAY, TYPE_BYTES, null); private static final Type TYPE_ARRAY_TIMESTAMP = new Type(Code.ARRAY, TYPE_TIMESTAMP, null); private static final Type TYPE_ARRAY_DATE = new Type(Code.ARRAY, TYPE_DATE, null); + private static final Type TYPE_ARRAY_INTERVAL = new Type(Code.ARRAY, TYPE_INTERVAL, null); private static final int AMBIGUOUS_FIELD = -1; private static final long serialVersionUID = -3076152125004114582L; @@ -183,6 +185,16 @@ public static Type date() { return TYPE_DATE; } + /** + * Returns the descriptor for the {@code INTERVAL} type: an interval which represents a time + * duration as a tuple of 3 values (months, days, nanoseconds). [Interval(months:-120000, days: + * -3660000, nanoseconds: -316224000000000000000), Interval(months:120000, days: 3660000, + * nanoseconds: 316224000000000000000)]. + */ + public static Type interval() { + return TYPE_INTERVAL; + } + /** Returns a descriptor for an array of {@code elementType}. */ public static Type array(Type elementType) { Preconditions.checkNotNull(elementType); @@ -213,6 +225,8 @@ public static Type array(Type elementType) { return TYPE_ARRAY_TIMESTAMP; case DATE: return TYPE_ARRAY_DATE; + case INTERVAL: + return TYPE_ARRAY_INTERVAL; default: return new Type(Code.ARRAY, elementType, null); } @@ -295,6 +309,7 @@ public enum Code { BYTES(TypeCode.BYTES, "bytea"), TIMESTAMP(TypeCode.TIMESTAMP, "timestamp with time zone"), DATE(TypeCode.DATE, "date"), + INTERVAL(TypeCode.INTERVAL, "interval"), ARRAY(TypeCode.ARRAY, "array"), STRUCT(TypeCode.STRUCT, "struct"); @@ -610,6 +625,8 @@ static Type fromProto(com.google.spanner.v1.Type proto) { return timestamp(); case DATE: return date(); + case INTERVAL: + return interval(); case PROTO: return proto(proto.getProtoTypeFqn()); case ENUM: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index c2c851d6dd..2d6371ea65 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -245,6 +245,15 @@ public static Value json(@Nullable String v) { return new JsonImpl(v == null, v); } + /** + * Returns a {@code INTERVAL} value. + * + * @param interval the value, which may be null + */ + public static Value interval(@Nullable Interval interval) { + return new IntervalImpl(interval == null, interval); + } + /** * Returns a {@code PG JSONB} value. * @@ -776,6 +785,16 @@ public static Value dateArray(@Nullable Iterable v) { return new DateArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); } + /** + * Returns an {@code ARRAY} value. + * + * @param v the source of element values. This may be {@code null} to produce a value for which + * {@code isNull()} is {@code true}. Individual elements may also be {@code null}. + */ + public static Value intervalArray(@Nullable Iterable v) { + return new IntervalArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); + } + /** * Returns an {@code ARRAY>} value. * @@ -915,6 +934,13 @@ public T getProtoEnum( */ public abstract Date getDate(); + /** + * Returns the value of a {@code INTERVAL}-typed instance. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public abstract Interval getInterval(); + /** * Returns the value of a {@code STRUCT}-typed instance. * @@ -1035,6 +1061,14 @@ public List getProtoEnumArray( */ public abstract List getDateArray(); + /** + * Returns the value of an {@code ARRAY}-typed instance. While the returned list itself + * will never be {@code null}, elements of that list may be null. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public abstract List getIntervalArray(); + /** * Returns the value of an {@code ARRAY>}-typed instance. While the returned list * itself will never be {@code null}, elements of that list may be null. @@ -1314,6 +1348,11 @@ public Date getDate() { throw defaultGetter(Type.date()); } + @Override + public Interval getInterval() { + throw defaultGetter(Type.interval()); + } + @Override public Struct getStruct() { if (getType().getCode() != Type.Code.STRUCT) { @@ -1378,6 +1417,11 @@ public List getDateArray() { throw defaultGetter(Type.array(Type.date())); } + @Override + public List getIntervalArray() { + throw defaultGetter(Type.array(Type.interval())); + } + @Override public List getStructArray() { if (getType().getCode() != Type.Code.ARRAY @@ -1795,6 +1839,29 @@ void valueToString(StringBuilder b) { } } + private static class IntervalImpl extends AbstractObjectValue { + + private IntervalImpl(boolean isNull, Interval value) { + super(isNull, Type.interval(), value); + } + + @Override + public Interval getInterval() { + checkNotNull(); + return value; + } + + @Override + void valueToString(StringBuilder b) { + b.append(value.ToISO8601()); + } + + @Override + com.google.protobuf.Value valueToProto() { + return com.google.protobuf.Value.newBuilder().setStringValue(value.ToISO8601()).build(); + } + } + private static class StringImpl extends AbstractObjectValue { private StringImpl(boolean isNull, @Nullable String value) { @@ -2797,6 +2864,29 @@ void appendElement(StringBuilder b, Date element) { } } + private static class IntervalArrayImpl extends AbstractArrayValue { + + private IntervalArrayImpl(boolean isNull, @Nullable List values) { + super(isNull, Type.interval(), values); + } + + @Override + public List getIntervalArray() { + checkNotNull(); + return value; + } + + @Override + void appendElement(StringBuilder b, Interval element) { + b.append(element.ToISO8601()); + } + + @Override + String elementToString(Interval element) { + return element.ToISO8601(); + } + } + private static class NumericArrayImpl extends AbstractArrayValue { private NumericArrayImpl(boolean isNull, @Nullable List values) { @@ -2940,6 +3030,8 @@ private Value getValue(int fieldIndex) { return Value.date(value.getDate(fieldIndex)); case TIMESTAMP: return Value.timestamp(value.getTimestamp(fieldIndex)); + case INTERVAL: + return Value.interval(value.getInterval(fieldIndex)); case PROTO: return Value.protoMessage(value.getBytes(fieldIndex), fieldType.getProtoTypeFqn()); case ENUM: @@ -2978,6 +3070,8 @@ private Value getValue(int fieldIndex) { return Value.dateArray(value.getDateList(fieldIndex)); case TIMESTAMP: return Value.timestampArray(value.getTimestampList(fieldIndex)); + case INTERVAL: + return Value.intervalArray(value.getIntervalList(fieldIndex)); case STRUCT: return Value.structArray(elementType, value.getStructList(fieldIndex)); case ARRAY: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java index 8386bd5c21..45b08ca582 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java @@ -165,6 +165,11 @@ public R to(@Nullable Date value) { return handle(Value.date(value)); } + /** Binds to {@code Value.interval(value)} */ + public R to(@Nullable Interval value) { + return handle(Value.interval(value)); + } + /** Binds a non-{@code NULL} struct value to {@code Value.struct(value)} */ public R to(Struct value) { return handle(Value.struct(value)); @@ -323,6 +328,11 @@ public R toDateArray(@Nullable Iterable values) { return handle(Value.dateArray(values)); } + /** Binds to {@code Value.intervalArray(values)} */ + public R toIntervalArray(@Nullable Iterable values) { + return handle(Value.intervalArray(values)); + } + /** Binds to {@code Value.structArray(fieldTypes, values)} */ public R toStructArray(Type elementType, @Nullable Iterable values) { return handle(Value.structArray(elementType, values)); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java index b5e4060ddd..bd8271bb99 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java @@ -19,12 +19,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ProtobufResultSet; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.Type; -import com.google.cloud.spanner.Value; +import com.google.cloud.spanner.*; import com.google.common.base.Preconditions; import com.google.protobuf.AbstractMessage; import com.google.protobuf.ProtocolMessageEnum; @@ -288,6 +283,18 @@ public Date getDate(String columnName) { return delegate.getDate(columnName); } + @Override + public Interval getInterval(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getInterval(columnIndex); + } + + @Override + public Interval getInterval(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getInterval(columnName); + } + @Override public Value getValue(int columnIndex) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); @@ -480,6 +487,18 @@ public List getDateList(String columnName) { return delegate.getDateList(columnName); } + @Override + public List getIntervalList(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getIntervalList(columnIndex); + } + + @Override + public List getIntervalList(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getIntervalList(columnName); + } + @Override public List getProtoMessageList(int columnIndex, T message) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java index bd7c794a0f..59229037e1 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java @@ -19,14 +19,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.ProtobufResultSet; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.Type; -import com.google.cloud.spanner.Value; +import com.google.cloud.spanner.*; import com.google.common.base.Preconditions; import com.google.protobuf.AbstractMessage; import com.google.protobuf.ProtocolMessageEnum; @@ -297,6 +290,18 @@ public Date getDate(String columnName) { return delegate.getDate(columnName); } + @Override + public Interval getInterval(int columnIndex) { + checkClosed(); + return delegate.getInterval(columnIndex); + } + + @Override + public Interval getInterval(String columnName) { + checkClosed(); + return delegate.getInterval(columnName); + } + @Override public Value getValue(int columnIndex) { checkClosed(); @@ -489,6 +494,18 @@ public List getDateList(String columnName) { return delegate.getDateList(columnName); } + @Override + public List getIntervalList(int columnIndex) { + checkClosed(); + return delegate.getIntervalList(columnIndex); + } + + @Override + public List getIntervalList(String columnName) { + checkClosed(); + return delegate.getIntervalList(columnName); + } + @Override public List getProtoMessageList(int columnIndex, T message) { checkClosed(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java index 595bbcaf26..8a036065bd 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java @@ -103,6 +103,11 @@ protected Date getDateInternal(int columnIndex) { return null; } + @Override + protected Interval getIntervalInternal(int columnIndex) { + return null; + } + @Override protected T getProtoMessageInternal(int columnIndex, T message) { return null; @@ -206,6 +211,11 @@ protected List getDateListInternal(int columnIndex) { return null; } + @Override + protected List getIntervalListInternal(int columnIndex) { + return null; + } + @Override protected List getStructListInternal(int columnIndex) { return null; @@ -301,6 +311,13 @@ public static Collection parameters() { "getDate", Collections.singletonList("getValue") }, + { + Type.interval(), + "getIntervalInternal", + Interval.parseFromString("P1Y2M3DT4H5M6.78912345S"), + "getInterval", + Collections.singletonList("getValue") + }, { Type.array(Type.bool()), "getBooleanArrayInternal", @@ -423,6 +440,16 @@ public static Collection parameters() { "getDateList", Collections.singletonList("getValue") }, + { + Type.array(Type.interval()), + "getIntervalListInternal", + Arrays.asList( + Interval.parseFromString("P1Y2M3DT4H5M6.78912345S"), + Interval.zeroInterval(), + Interval.parseFromString("P-1Y2M-3DT-4H5M6.78912345S")), + "getIntervalList", + Collections.singletonList("getValue") + }, { Type.array(Type.struct(StructField.of("f1", Type.int64()))), "getStructListInternal", diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java index 62336163ea..265da756b9 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java @@ -42,6 +42,7 @@ import com.google.spanner.v1.ResultSetStats; import com.google.spanner.v1.Transaction; import java.math.BigDecimal; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -548,6 +549,14 @@ public void serialization() { Value.timestamp(null), Value.date(Date.fromYearMonthDay(2017, 4, 17)), Value.date(null), + Value.interval( + Interval.builder() + .setDays(10) + .setMonths(100) + .setMicros(1000) + .setNanoFractions((short) 10) + .build()), + Value.interval(null), Value.stringArray(ImmutableList.of("one", "two")), Value.stringArray(null), Value.boolArray(new boolean[] {true, false}), @@ -570,6 +579,9 @@ public void serialization() { ImmutableList.of( Date.fromYearMonthDay(2017, 4, 17), Date.fromYearMonthDay(2017, 5, 18))), Value.dateArray(null), + Value.intervalArray( + ImmutableList.of(Interval.zeroInterval(), Interval.fromMonthsDaysMicros(10, 20, 30))), + Value.intervalArray(null), Value.struct(s(null, 30)), Value.struct(structType, null), Value.structArray(structType, Arrays.asList(s("def", 10), null)), @@ -735,6 +747,22 @@ public void getDate() { assertThat(resultSet.getDate(0)).isEqualTo(Date.fromYearMonthDay(2018, 5, 29)); } + @Test + public void getInterval() { + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata(makeMetadata(Type.struct(Type.StructField.of("f", Type.interval())))) + .addValues( + Value.interval(Interval.fromMonthsDaysNanos(10, 20, BigInteger.valueOf(12345678))) + .toProto()) + .build()); + consumer.onCompleted(); + + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getInterval(0)) + .isEqualTo(Interval.fromMonthsDaysNanos(10, 20, BigInteger.valueOf(12345678))); + } + @Test public void getTimestamp() { consumer.onPartialResultSet( @@ -988,6 +1016,24 @@ public void getDateList() { assertThat(resultSet.getDateList(0)).isEqualTo(dateList); } + @Test + public void getIntervalList() { + List intervalList = new ArrayList<>(); + intervalList.add(Interval.fromMonthsDaysMicros(10, 20, 100)); + intervalList.add(Interval.fromMonthsDaysNanos(-10, -20, BigInteger.valueOf(134520))); + + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata( + makeMetadata(Type.struct(Type.StructField.of("f", Type.array(Type.interval()))))) + .addValues(Value.intervalArray(intervalList).toProto()) + .build()); + consumer.onCompleted(); + + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getIntervalList(0)).isEqualTo(intervalList); + } + @Test public void getJsonList() { List jsonList = new ArrayList<>(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntervalTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntervalTest.java new file mode 100644 index 0000000000..2fcc505adb --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntervalTest.java @@ -0,0 +1,162 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed 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 com.google.cloud.spanner; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import java.math.BigInteger; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class IntervalTest { + + @Test + public void testConstructionAndInitialization() { + Interval interval1 = + Interval.builder().setMonths(1).setDays(2).setMicros(3).setNanoFractions((short) 4).build(); + assertThat(interval1.months()).isEqualTo(1); + assertThat(interval1.days()).isEqualTo(2); + assertThat(interval1.micros()).isEqualTo(3); + assertThat(interval1.nanoFractions()).isEqualTo((short) 4); + + Interval interval2 = Interval.ofMonths(5); + assertThat(interval2.months()).isEqualTo(5); + assertThat(interval2.days()).isEqualTo(0); + assertThat(interval2.micros()).isEqualTo(0); + assertThat(interval2.nanoFractions()).isEqualTo((short) 0); + + Interval interval3 = Interval.ofDays(10); + assertThat(interval3.months()).isEqualTo(0); + assertThat(interval3.days()).isEqualTo(10); + assertThat(interval3.micros()).isEqualTo(0); + assertThat(interval3.nanoFractions()).isEqualTo((short) 0); + + Interval interval4 = Interval.fromMonthsDaysMicros(2, 5, 1000); + assertThat(interval4.months()).isEqualTo(2); + assertThat(interval4.days()).isEqualTo(5); + assertThat(interval4.micros()).isEqualTo(1000); + assertThat(interval4.nanoFractions()).isEqualTo((short) 0); + + Interval interval5 = Interval.fromMonthsDaysNanos(3, 10, BigInteger.valueOf(5000)); + assertThat(interval5.months()).isEqualTo(3); + assertThat(interval5.days()).isEqualTo(10); + assertThat(interval5.micros()).isEqualTo(5); + assertThat(interval5.nanoFractions()).isEqualTo((short) 0); + } + + @Test + public void testUnitConversions() { + Interval interval = + Interval.builder().setMonths(1).setDays(2).setMicros(3).setNanoFractions((short) 4).build(); + assertThat(interval.nanos()) + .isEqualTo( + BigInteger.valueOf(3) + .multiply(BigInteger.valueOf(Interval.NANOS_PER_MICRO)) + .add(BigInteger.valueOf(4))); + assertThat(interval.getAsMicros()) + .isEqualTo(1 * Interval.MICROS_PER_MONTH + 2 * Interval.MICROS_PER_DAY + 3); + assertThat(interval.getAsNanos()) + .isEqualTo( + BigInteger.valueOf(interval.getAsMicros()) + .multiply(BigInteger.valueOf(Interval.NANOS_PER_MICRO)) + .add(BigInteger.valueOf(4))); + } + + @Test + public void testParsingAndFormatting() { + Interval interval1 = Interval.parseFromString("P1Y2M3DT4H5M6.78912345S"); + assertThat(interval1.months()).isEqualTo(14); + assertThat(interval1.days()).isEqualTo(3); + assertThat(interval1.micros()) + .isEqualTo(4 * Interval.MICROS_PER_HOUR + 5 * Interval.MICROS_PER_MINUTE + 6789123); + assertThat(interval1.nanoFractions()).isEqualTo((short) 450); + assertThat(interval1.ToISO8601()).isEqualTo("P1Y2M3DT4H5M6.789123450S"); + + Interval interval2 = Interval.parseFromString("P1Y"); + assertThat(interval2.months()).isEqualTo(12); + assertThat(interval2.days()).isEqualTo(0); + assertThat(interval2.micros()).isEqualTo(0); + assertThat(interval2.nanoFractions()).isEqualTo((short) 0); + assertThat(interval2.ToISO8601()).isEqualTo("P1Y"); + + Interval interval3 = Interval.parseFromString("P0Y"); + assertThat(interval3.months()).isEqualTo(0); + assertThat(interval3.days()).isEqualTo(0); + assertThat(interval3.micros()).isEqualTo(0); + assertThat(interval3.nanoFractions()).isEqualTo((short) 0); + assertThat(interval3.ToISO8601()).isEqualTo("P0Y"); + + assertThrows(SpannerException.class, () -> Interval.parseFromString("invalid")); + assertThrows(SpannerException.class, () -> Interval.parseFromString("P")); + assertThrows(SpannerException.class, () -> Interval.parseFromString("P0")); + assertThrows(SpannerException.class, () -> Interval.parseFromString("P1M1M")); + assertThrows(SpannerException.class, () -> Interval.parseFromString("PT0")); + assertThrows(SpannerException.class, () -> Interval.parseFromString("PT0.1234567890S")); + } + + @Test + public void testComparisonAndEquality() { + Interval interval1 = Interval.fromMonthsDaysMicros(1, 2, 3); + Interval interval2 = Interval.fromMonthsDaysMicros(1, 2, 3); + Interval interval3 = Interval.fromMonthsDaysMicros(2, 2, 3); + + assertThat(interval1.compareTo(interval2)).isEqualTo(0); + assertThat(interval1.compareTo(interval3)).isLessThan(0); + assertThat(interval3.compareTo(interval1)).isGreaterThan(0); + } + + @Test + public void testOfSeconds() { + Interval interval = Interval.ofSeconds(10); + assertThat(interval.getAsMicros()).isEqualTo(10 * Interval.MICROS_PER_SECOND); + } + + @Test + public void testOfMilliseconds() { + Interval interval = Interval.ofMilliseconds(200); + assertThat(interval.getAsMicros()).isEqualTo(200 * Interval.MICROS_PER_MILLI); + } + + @Test + public void testOfMicros() { + Interval interval = Interval.ofMicros(3000000); + assertThat(interval.getAsMicros()).isEqualTo(3000000); + } + + @Test + public void testOfNanos() { + Interval interval = Interval.ofNanos(BigInteger.valueOf(4000000000L)); + assertThat(interval.getAsNanos()).isEqualTo(BigInteger.valueOf(4000000000L)); + } + + @Test + public void testZeroInterval() { + Interval interval = Interval.zeroInterval(); + assertThat(interval.getAsNanos()).isEqualTo(BigInteger.ZERO); + } + + @Test + public void testBuilder_invalidNanoFractions() { + assertThrows( + SpannerException.class, () -> Interval.builder().setNanoFractions((short) -1000).build()); + assertThrows( + SpannerException.class, () -> Interval.builder().setNanoFractions((short) 1000).build()); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index 9f0a2822d8..fd5dfa4055 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -1294,6 +1294,9 @@ private Statement buildStatement( case DATE: builder.bind(fieldName).toDateArray(null); break; + case INTERVAL: + builder.bind(fieldName).toIntervalArray(null); + break; case FLOAT32: builder.bind(fieldName).toFloat32Array((Iterable) null); break; @@ -1340,6 +1343,9 @@ private Statement buildStatement( case DATE: builder.bind(fieldName).to((Date) null); break; + case INTERVAL: + builder.bind(fieldName).to((Interval) null); + break; case FLOAT32: builder.bind(fieldName).to((Float) null); break; @@ -1408,6 +1414,14 @@ private Statement buildStatement( GrpcStruct.decodeArrayValue( com.google.cloud.spanner.Type.date(), value.getListValue())); break; + case INTERVAL: + builder + .bind(fieldName) + .toIntervalArray( + (Iterable) + GrpcStruct.decodeArrayValue( + com.google.cloud.spanner.Type.interval(), value.getListValue())); + break; case FLOAT32: builder .bind(fieldName) @@ -1499,6 +1513,9 @@ private Statement buildStatement( case DATE: builder.bind(fieldName).to(Date.parseDate(value.getStringValue())); break; + case INTERVAL: + builder.bind(fieldName).to(Interval.parseFromString(value.getStringValue())); + break; case FLOAT32: builder.bind(fieldName).to((float) value.getNumberValue()); break; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java index 058429d3ba..251b1a555e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java @@ -43,6 +43,7 @@ public class RandomResultSetGenerator { Type.newBuilder().setCode(TypeCode.STRING).build(), Type.newBuilder().setCode(TypeCode.BYTES).build(), Type.newBuilder().setCode(TypeCode.DATE).build(), + Type.newBuilder().setCode(TypeCode.INTERVAL).build(), Type.newBuilder().setCode(TypeCode.TIMESTAMP).build(), Type.newBuilder() .setCode(TypeCode.ARRAY) @@ -72,6 +73,10 @@ public class RandomResultSetGenerator { .setCode(TypeCode.ARRAY) .setArrayElementType(Type.newBuilder().setCode(TypeCode.DATE)) .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.INTERVAL)) + .build(), Type.newBuilder() .setCode(TypeCode.ARRAY) .setArrayElementType(Type.newBuilder().setCode(TypeCode.TIMESTAMP)) @@ -142,6 +147,16 @@ private void setRandomValue(Value.Builder builder, Type type) { random.nextInt(2019) + 1, random.nextInt(11) + 1, random.nextInt(28) + 1); builder.setStringValue(date.toString()); break; + case INTERVAL: + Interval interval = + Interval.builder() + .setMonths(10) + .setDays(45) + .setMicros(-89) + .setNanoFractions((short) 45) + .build(); + builder.setStringValue(interval.ToISO8601()); + break; case FLOAT64: builder.setNumberValue(random.nextDouble()); break; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java index 3ca550caa2..40523a1c8d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java @@ -70,6 +70,7 @@ public void resultSetIteration() { int year = 2018; int month = 5; int day = 26; + Interval interval = Interval.parseFromString("P1Y2M3DT5H7M8.967589762S"); boolean[] boolArray = {true, false, true, true, false}; long[] longArray = {Long.MAX_VALUE, Long.MIN_VALUE, 0, 1, -1}; double[] doubleArray = {Double.MIN_VALUE, Double.MAX_VALUE, 0, 1, -1, 1.2341}; @@ -92,6 +93,9 @@ public void resultSetIteration() { Date[] dateArray = { Date.fromYearMonthDay(1, 2, 3), Date.fromYearMonthDay(4, 5, 6), Date.fromYearMonthDay(7, 8, 9) }; + Interval[] intervalArray = { + Interval.zeroInterval(), Interval.parseFromString("P1Y2M3DT-5H-7M8.9675S") + }; String[] stringArray = {"abc", "def", "ghi"}; String[] jsonArray = {"{}", "{\"color\":\"red\",\"value\":\"#f00\"}", "[]"}; AbstractMessage[] protoMessageArray = { @@ -114,6 +118,7 @@ public void resultSetIteration() { Type.StructField.of("byteVal", Type.bytes()), Type.StructField.of("timestamp", Type.timestamp()), Type.StructField.of("date", Type.date()), + Type.StructField.of("interval", Type.interval()), Type.StructField.of( "protoMessage", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), Type.StructField.of( @@ -126,6 +131,7 @@ public void resultSetIteration() { Type.StructField.of("byteArray", Type.array(Type.bytes())), Type.StructField.of("timestampArray", Type.array(Type.timestamp())), Type.StructField.of("dateArray", Type.array(Type.date())), + Type.StructField.of("intervalArray", Type.array(Type.interval())), Type.StructField.of("stringArray", Type.array(Type.string())), Type.StructField.of("jsonArray", Type.array(Type.json())), Type.StructField.of("pgJsonbArray", Type.array(Type.pgJsonb())), @@ -163,6 +169,8 @@ public void resultSetIteration() { .to(Timestamp.ofTimeMicroseconds(usecs)) .set("date") .to(Date.fromYearMonthDay(year, month, day)) + .set("interval") + .to(interval) .set("protoMessage") .to(protoMessageVal) .set("protoEnum") @@ -183,6 +191,8 @@ public void resultSetIteration() { .to(Value.timestampArray(Arrays.asList(timestampArray))) .set("dateArray") .to(Value.dateArray(Arrays.asList(dateArray))) + .set("intervalArray") + .to(Value.intervalArray(Arrays.asList(intervalArray))) .set("stringArray") .to(Value.stringArray(Arrays.asList(stringArray))) .set("jsonArray") @@ -228,6 +238,8 @@ public void resultSetIteration() { .to(Timestamp.ofTimeMicroseconds(usecs)) .set("date") .to(Date.fromYearMonthDay(year, month, day)) + .set("interval") + .to(Value.interval(interval)) .set("protoMessage") .to(protoMessageVal) .set("protoEnum") @@ -248,6 +260,8 @@ public void resultSetIteration() { .to(Value.timestampArray(Arrays.asList(timestampArray))) .set("dateArray") .to(Value.dateArray(Arrays.asList(dateArray))) + .set("intervalArray") + .to(Value.intervalArray(Arrays.asList(intervalArray))) .set("stringArray") .to(Value.stringArray(Arrays.asList(stringArray))) .set("jsonArray") @@ -339,6 +353,11 @@ public void resultSetIteration() { assertThat(rs.getDate("date")).isEqualTo(Date.fromYearMonthDay(year, month, day)); assertThat(rs.getValue("date")).isEqualTo(Value.date(Date.fromYearMonthDay(year, month, day))); + assertThat(rs.getInterval(columnIndex)).isEqualTo(interval); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.interval(interval)); + assertThat(rs.getInterval("interval")).isEqualTo(interval); + assertThat(rs.getValue("interval")).isEqualTo(Value.interval(interval)); + assertEquals(protoMessageVal, rs.getProtoMessage(columnIndex, SingerInfo.getDefaultInstance())); assertEquals(Value.protoMessage(protoMessageVal), rs.getValue(columnIndex++)); assertEquals( @@ -400,6 +419,12 @@ public void resultSetIteration() { assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.dateArray(Arrays.asList(dateArray))); assertThat(rs.getDateList("dateArray")).isEqualTo(Arrays.asList(dateArray)); assertThat(rs.getValue("dateArray")).isEqualTo(Value.dateArray(Arrays.asList(dateArray))); + assertThat(rs.getIntervalList(columnIndex)).isEqualTo(Arrays.asList(intervalArray)); + assertThat(rs.getValue(columnIndex++)) + .isEqualTo(Value.intervalArray(Arrays.asList(intervalArray))); + assertThat(rs.getIntervalList("intervalArray")).isEqualTo(Arrays.asList(intervalArray)); + assertThat(rs.getValue("intervalArray")) + .isEqualTo(Value.intervalArray(Arrays.asList(intervalArray))); assertThat(rs.getStringList(columnIndex)).isEqualTo(Arrays.asList(stringArray)); assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.stringArray(Arrays.asList(stringArray))); assertThat(rs.getStringList("stringArray")).isEqualTo(Arrays.asList(stringArray)); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java index aea799aa15..d7fcb38050 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java @@ -240,6 +240,16 @@ Type newType() { }.test(); } + @Test + public void interval() { + new ScalarTypeTester(Code.INTERVAL, TypeCode.INTERVAL) { + @Override + Type newType() { + return Type.interval(); + } + }.test(); + } + abstract static class ArrayTypeTester { private final Type.Code expectedElementCode; private final TypeCode expectedElementTypeCode; @@ -428,6 +438,16 @@ Type newElementType() { }.test(); } + @Test + public void intervalArray() { + new ArrayTypeTester(Type.Code.INTERVAL, TypeCode.INTERVAL, true) { + @Override + Type newElementType() { + return Type.interval(); + } + }.test(); + } + @Test public void protoArray() { new ArrayTypeTester(Type.Code.PROTO, TypeCode.PROTO, "com.google.temp", false) { @@ -615,6 +635,7 @@ public void testGoogleSQLTypeNames() { assertEquals("STRING", Type.string().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("BYTES", Type.bytes().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("DATE", Type.date().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); + assertEquals("INTERVAL", Type.interval().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("TIMESTAMP", Type.timestamp().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("JSON", Type.json().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("NUMERIC", Type.numeric().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); @@ -632,6 +653,9 @@ public void testGoogleSQLTypeNames() { "ARRAY", Type.array(Type.bytes()).getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals( "ARRAY", Type.array(Type.date()).getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); + assertEquals( + "ARRAY", + Type.array(Type.interval()).getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals( "ARRAY", Type.array(Type.timestamp()).getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); @@ -650,6 +674,7 @@ public void testPostgreSQLTypeNames() { assertEquals("character varying", Type.string().getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals("bytea", Type.bytes().getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals("date", Type.date().getSpannerTypeName(Dialect.POSTGRESQL)); + assertEquals("interval", Type.interval().getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals( "timestamp with time zone", Type.timestamp().getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals("jsonb", Type.pgJsonb().getSpannerTypeName(Dialect.POSTGRESQL)); @@ -663,6 +688,7 @@ public void testPostgreSQLTypeNames() { "character varying[]", Type.array(Type.string()).getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals("bytea[]", Type.array(Type.bytes()).getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals("date[]", Type.array(Type.date()).getSpannerTypeName(Dialect.POSTGRESQL)); + assertEquals("interval[]", Type.array(Type.interval()).getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals( "timestamp with time zone[]", Type.array(Type.timestamp()).getSpannerTypeName(Dialect.POSTGRESQL)); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java index 23128ad52b..832b3e17d8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java @@ -331,6 +331,10 @@ public static Date defaultDate() { return Date.fromYearMonthDay(2016, 9, 15); } + public static Interval defaultInterval() { + return Interval.zeroInterval(); + } + public static boolean[] defaultBooleanArray() { return new boolean[] {false, true}; } @@ -388,6 +392,41 @@ public static Iterable defaultDateIterable() { return Arrays.asList(Date.fromYearMonthDay(2016, 9, 15), Date.fromYearMonthDay(2016, 9, 14)); } + public static Interval[] defaultIntervalArray() { + return new Interval[] { + Interval.builder() + .setMonths(-10) + .setDays(-100) + .setMicros(-1000) + .setNanoFractions((short) 100) + .build(), + Interval.zeroInterval(), + Interval.builder() + .setMonths(10) + .setDays(100) + .setMicros(1000) + .setNanoFractions((short) 100) + .build() + }; + } + + public static Iterable defaultIntervalIterable() { + return Arrays.asList( + Interval.builder() + .setMonths(-10) + .setDays(-100) + .setMicros(-1000) + .setNanoFractions((short) 100) + .build(), + Interval.zeroInterval(), + Interval.builder() + .setMonths(10) + .setDays(100) + .setMicros(1000) + .setNanoFractions((short) 100) + .build()); + } + static Object getDefault(java.lang.reflect.Type type) throws InvocationTargetException, IllegalAccessException { for (Method method : DefaultValues.class.getMethods()) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index 92b63913fd..6b2544c191 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -44,6 +44,7 @@ import com.google.protobuf.NullValue; import java.io.Serializable; import java.math.BigDecimal; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -731,6 +732,29 @@ public void dateNull() { assertEquals("NULL", v.getAsString()); } + @Test + public void interval() { + String interval = "P1Y2M3DT67H45M5.123478678S"; + Interval t = Interval.parseFromString(interval); + Value v = Value.interval(t); + assertThat(v.getType()).isEqualTo(Type.interval()); + assertThat(v.isNull()).isFalse(); + assertThat(v.getInterval()).isSameInstanceAs(t); + assertThat(v.toString()).isEqualTo(interval); + assertEquals(interval, v.getAsString()); + } + + @Test + public void intervalNull() { + Value v = Value.interval(null); + assertThat(v.getType()).isEqualTo(Type.interval()); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = assertThrows(IllegalStateException.class, v::getInterval); + assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); + } + @Test public void protoMessage() { SingerInfo singerInfo = SingerInfo.newBuilder().setSingerId(111).setGenre(Genre.FOLK).build(); @@ -1359,6 +1383,31 @@ public void dateArrayNull() { assertEquals("NULL", v.getAsString()); } + @Test + public void intervalArray() { + Interval interval1 = Interval.parseFromString("P123Y34M678DT478H345M345.76857863S"); + Interval interval2 = Interval.parseFromString("P-123Y-34M678DT-478H-345M-345.76857863S"); + + Value v = Value.intervalArray(Arrays.asList(interval1, null, interval2)); + assertThat(v.isNull()).isFalse(); + assertThat(v.getIntervalArray()).containsExactly(interval1, null, interval2).inOrder(); + assertThat(v.toString()) + .isEqualTo("[" + interval1.ToISO8601() + ",NULL," + interval2.ToISO8601() + "]"); + assertEquals( + String.format("[%s,NULL,%s]", interval1.ToISO8601(), interval2.ToISO8601()), + v.getAsString()); + } + + @Test + public void intervalArrayNull() { + Value v = Value.intervalArray(null); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = assertThrows(IllegalStateException.class, v::getIntervalArray); + assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); + } + @Test public void protoMessageArray() { SingerInfo singerInfo1 = SingerInfo.newBuilder().setSingerId(111).setGenre(Genre.FOLK).build(); @@ -1654,6 +1703,14 @@ public void testValueToProto() { com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), Value.date(null).toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder().setStringValue("P1Y2M3DT5H6M3.624567878S").build(), + Value.interval(Interval.fromMonthsDaysNanos(14, 3, BigInteger.valueOf(18363624567878L))) + .toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), + Value.interval(null).toProto()); + assertEquals( com.google.protobuf.Value.newBuilder() .setStringValue("2012-04-10T15:16:17.123456789Z") @@ -1790,6 +1847,25 @@ public void testValueToProto() { .build()))) .build(), Value.dateArray(Arrays.asList(Date.fromYearMonthDay(2010, 2, 28), null)).toProto()); + + assertEquals( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addAllValues( + Arrays.asList( + com.google.protobuf.Value.newBuilder() + .setStringValue("P1Y2M3DT5H6M2.456787800S") + .build(), + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()))) + .build(), + Value.intervalArray( + Arrays.asList( + Interval.fromMonthsDaysNanos(14, 3, new BigInteger("18362456787800")), null)) + .toProto()); + assertEquals( com.google.protobuf.Value.newBuilder() .setListValue( @@ -2222,6 +2298,13 @@ public void testEqualsHashCode() { Value.dateArray(Arrays.asList(null, Date.fromYearMonthDay(2018, 2, 26)))); tester.addEqualityGroup(Value.dateArray(null)); + tester.addEqualityGroup( + Value.intervalArray( + Arrays.asList(null, Interval.fromMonthsDaysNanos(14, 3, BigInteger.valueOf(0)))), + Value.intervalArray( + Arrays.asList(null, Interval.fromMonthsDaysNanos(14, 3, BigInteger.valueOf(0))))); + tester.addEqualityGroup(Value.intervalArray(null)); + tester.addEqualityGroup( Value.structArray(structType1, Arrays.asList(structValue1, null)), Value.structArray(structType1, Arrays.asList(structValue2, null))); @@ -2276,6 +2359,9 @@ public void testGetAsString() { "2023-01-10T18:59:00Z", Value.timestamp(Timestamp.parseTimestamp("2023-01-10T18:59:00Z")).getAsString()); assertEquals("2023-01-10", Value.date(Date.parseDate("2023-01-10")).getAsString()); + assertEquals( + "P1Y2M3DT4H5M6.789123456S", + Value.interval(Interval.parseFromString("P1Y2M3DT4H5M6.789123456S")).getAsString()); Random random = new Random(); byte[] bytes = new byte[random.nextInt(256)]; @@ -2378,6 +2464,14 @@ public void serialization() { reserializeAndAssert(Value.date(Date.fromYearMonthDay(2018, 2, 26))); reserializeAndAssert(Value.dateArray(Arrays.asList(null, Date.fromYearMonthDay(2018, 2, 26)))); + reserializeAndAssert(Value.interval(null)); + reserializeAndAssert( + Value.interval(Interval.fromMonthsDaysNanos(15, 7, BigInteger.valueOf(1234567891)))); + reserializeAndAssert( + Value.intervalArray( + Arrays.asList( + null, Interval.fromMonthsDaysNanos(15, 7, BigInteger.valueOf(1234567891))))); + BrokenSerializationList of = BrokenSerializationList.of("a", "b"); reserializeAndAssert(Value.stringArray(of)); reserializeAndAssert(Value.stringArray(null)); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java index e13cfa91c1..0ee7713b44 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java @@ -25,18 +25,11 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.ResultSets; +import com.google.cloud.spanner.*; import com.google.cloud.spanner.SingerProto.Genre; import com.google.cloud.spanner.SingerProto.SingerInfo; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Struct.Builder; -import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; -import com.google.cloud.spanner.Value; import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement; import com.google.common.collect.ImmutableList; import java.math.BigDecimal; @@ -81,6 +74,8 @@ public class ChecksumResultSetTest { .to(Timestamp.parseTimestamp("2022-08-04T11:20:00.123456789Z")) .set("date") .to(Date.fromYearMonthDay(2022, 8, 3)) + .set("interval") + .to(Interval.parseFromString("P8Y2M3DT4H5M6.789123456S")) .set("boolArray") .to(Value.boolArray(Arrays.asList(Boolean.FALSE, null, Boolean.TRUE))) .set("longArray") @@ -108,6 +103,13 @@ public class ChecksumResultSetTest { .to( Value.dateArray( Arrays.asList(Date.parseDate("2000-01-01"), null, Date.parseDate("2022-08-03")))) + .set("intervalArray") + .to( + Value.intervalArray( + Arrays.asList( + Interval.parseFromString("P1Y2M-3DT4H5M6.789123456S"), + null, + Interval.parseFromString("P-1Y-2M-3DT-4H-5M-6.789123456S")))) .set("stringArray") .to(Value.stringArray(Arrays.asList("test2", null, "test1"))) .set("jsonArray") @@ -150,6 +152,7 @@ public void testRetry() { Type.StructField.of("byteVal", Type.bytes()), Type.StructField.of("timestamp", Type.timestamp()), Type.StructField.of("date", Type.date()), + Type.StructField.of("interval", Type.interval()), Type.StructField.of("boolArray", Type.array(Type.bool())), Type.StructField.of("longArray", Type.array(Type.int64())), Type.StructField.of("doubleArray", Type.array(Type.float64())), @@ -159,6 +162,7 @@ public void testRetry() { Type.StructField.of("byteArray", Type.array(Type.bytes())), Type.StructField.of("timestampArray", Type.array(Type.timestamp())), Type.StructField.of("dateArray", Type.array(Type.date())), + Type.StructField.of("intervalArray", Type.array(Type.interval())), Type.StructField.of("stringArray", Type.array(Type.string())), Type.StructField.of("jsonArray", Type.array(Type.json())), Type.StructField.of("pgJsonbArray", Type.array(Type.pgJsonb())), @@ -200,6 +204,8 @@ public void testRetry() { .to(Timestamp.parseTimestamp("2022-08-04T10:19:00.123456789Z")) .set("date") .to(Date.fromYearMonthDay(2022, 8, 4)) + .set("interval") + .to(Interval.parseFromString("P1Y2M3DT4H5M6.789123456S")) .set("boolArray") .to(Value.boolArray(Arrays.asList(Boolean.TRUE, null, Boolean.FALSE))) .set("longArray") @@ -228,6 +234,13 @@ public void testRetry() { Value.dateArray( Arrays.asList( Date.parseDate("2000-01-01"), null, Date.parseDate("2022-08-04")))) + .set("intervalArray") + .to( + Value.intervalArray( + Arrays.asList( + Interval.parseFromString("P1Y2M3DT4H5M6.789123456S"), + null, + Interval.parseFromString("P-1Y-2M-3DT-4H-5M-6.789123456S")))) .set("stringArray") .to(Value.stringArray(Arrays.asList("test1", null, "test2"))) .set("jsonArray") @@ -282,6 +295,8 @@ public void testRetry() { .to((Timestamp) null) .set("date") .to((Date) null) + .set("interval") + .to((Interval) null) .set("boolArray") .toBoolArray((Iterable) null) .set("longArray") @@ -300,6 +315,8 @@ public void testRetry() { .toTimestampArray(null) .set("dateArray") .toDateArray(null) + .set("intervalArray") + .toIntervalArray(null) .set("stringArray") .toStringArray(null) .set("jsonArray") diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITIntervalTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITIntervalTest.java new file mode 100644 index 0000000000..710e9ed483 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITIntervalTest.java @@ -0,0 +1,214 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.spanner.it; + +import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; +import static com.google.common.base.Strings.isNullOrEmpty; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.*; +import com.google.cloud.spanner.connection.ConnectionOptions; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@Category(ParallelIntegrationTest.class) +@RunWith(Parameterized.class) +public class ITIntervalTest { + + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + + @Parameterized.Parameters(name = "Dialect = {0}") + public static List data() { + return Arrays.asList( + new DialectTestParameter(Dialect.GOOGLE_STANDARD_SQL), + new DialectTestParameter(Dialect.POSTGRESQL)); + } + + @Parameterized.Parameter() public DialectTestParameter dialect; + + private static DatabaseClient googleStandardSQLClient; + private static DatabaseClient postgreSQLClient; + + private static final String[] GOOGLE_STANDARD_SQL_SCHEMA = + new String[] { + "CREATE TABLE IntervalTable (\n" + + " key STRING(MAX),\n" + + " slo_days INT64,\n" + + " update_time TIMESTAMP,\n" + + " expiry_days INT64 AS (EXTRACT(DAY FROM make_interval(day => GREATEST(LEAST(slo_days, 365), 1)))),\n" + + " interval_array_len bigint AS (ARRAY_LENGTH(ARRAY[INTERVAL '1-2 3 4:5:6' YEAR TO SECOND]))\n" + + ") PRIMARY KEY (key);" + }; + + private static final String[] POSTGRESQL_SCHEMA = + new String[] { + "CREATE TABLE IntervalTable (\n" + + " key text primary key,\n" + + " slo_days bigint,\n" + + " update_time timestamptz,\n" + + " expiry_days bigint GENERATED ALWAYS AS (EXTRACT(DAY FROM make_interval(days =>GREATEST(LEAST(slo_days, 365), 1)))) STORED,\n" + + " interval_array_len bigint GENERATED ALWAYS AS (ARRAY_LENGTH(ARRAY[INTERVAL '1-2 3 4:5:6'], 1)) STORED\n" + + ");" + }; + + private static DatabaseClient client; + + private static boolean isUsingCloudDevel() { + String jobType = System.getenv("JOB_TYPE"); + + // Assumes that the jobType contains the string "cloud-devel" to signal that + // the environment is cloud-devel. + return !isNullOrEmpty(jobType) && jobType.contains("cloud-devel"); + } + + @BeforeClass + public static void setUpDatabase() + throws ExecutionException, InterruptedException, TimeoutException { + assumeTrue("Interval is supported only in Cloud-Devel for now", isUsingCloudDevel()); + assumeFalse("Emulator does not support Interval yet", isUsingEmulator()); + + Database googleStandardSQLDatabase = + env.getTestHelper().createTestDatabase(GOOGLE_STANDARD_SQL_SCHEMA); + googleStandardSQLClient = env.getTestHelper().getDatabaseClient(googleStandardSQLDatabase); + Database postgreSQLDatabase = + env.getTestHelper() + .createTestDatabase(Dialect.POSTGRESQL, Arrays.asList(POSTGRESQL_SCHEMA)); + postgreSQLClient = env.getTestHelper().getDatabaseClient(postgreSQLDatabase); + } + + @Before + public void before() { + client = + dialect.dialect == Dialect.GOOGLE_STANDARD_SQL ? googleStandardSQLClient : postgreSQLClient; + } + + @AfterClass + public static void tearDown() throws Exception { + ConnectionOptions.closeSpanner(); + } + + /** Sequence used to generate unique keys. */ + private static int seq; + + private static String uniqueString() { + return String.format("k%04d", seq++); + } + + private String lastKey; + + private Timestamp write(Mutation m) { + return client.write(Collections.singletonList(m)); + } + + private Mutation.WriteBuilder baseInsert() { + return Mutation.newInsertOrUpdateBuilder("T").set("Key").to(lastKey = uniqueString()); + } + + private Struct readRow(String table, String key, String... columns) { + return client + .singleUse(TimestampBound.strong()) + .readRow(table, Key.of(key), Arrays.asList(columns)); + } + + private Struct readLastRow(String... columns) { + return readRow("T", lastKey, columns); + } + + @Test + public void writeToTableWithIntervalExpressions() { + write( + baseInsert() + .set("slo_days") + .to(5) + .set("update_time") + .to(Timestamp.ofTimeMicroseconds(12345678L)) + .build()); + Struct row = readLastRow("expiryDays", "interval_array_len"); + assertFalse(row.isNull(0)); + assertEquals(5, row.getLong(0)); + assertFalse(row.isNull(1)); + assertEquals(1, row.getLong(1)); + } + + @Test + public void queryInterval() { + try (ResultSet resultSet = + client + .singleUse() + .executeQuery(Statement.of("SELECT INTERVAL '1' DAY + INTERVAL '1' MONTH AS Col1"))) { + assertTrue(resultSet.next()); + assertTrue(resultSet.getInterval(0).equals(Interval.fromMonthsDaysMicros(1, 1, 0))); + } + } + + @Test + public void queryWithUntypedIntervalParam() { + String query; + if (dialect.dialect == Dialect.POSTGRESQL) { + query = "SELECT (INTERVAL '1' DAY > $1) AS Col1"; + } else { + query = "SELECT (INTERVAL '1' DAY > @p1) AS Col1"; + } + + try (ResultSet resultSet = + client + .singleUse() + .executeQuery( + Statement.newBuilder(query) + .bind("p1") + .to( + Value.untyped( + com.google.protobuf.Value.newBuilder() + .setStringValue("PT1.5S") + .build())) + .build())) { + assertTrue(resultSet.next()); + assertTrue(resultSet.getBoolean(0)); + } + } + + @Test + public void queryIntervalArray() { + String query = + "SELECT ARRAY[CAST('P1Y2M3DT4H5M6.789123S' AS INTERVAL), null, CAST('P-1Y-2M-3DT-4H-5M-6.789123S' AS INTERVAL)] AS Col1"; + try (ResultSet resultSet = client.singleUse().executeQuery(Statement.of(query))) { + assertTrue(resultSet.next()); + assertTrue( + Arrays.asList( + Interval.parseFromString("P1Y2M3DT4H5M6.789123S"), + null, + Interval.parseFromString("P-1Y-2M-3DT-4H-5M-6.789123S")) + .equals(resultSet.getIntervalList(0))); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java index 18044c452b..b5d5ceb2f6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java @@ -30,22 +30,9 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.Database; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.Dialect; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.IntegrationTestEnv; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.*; import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; -import com.google.cloud.spanner.Value; import com.google.cloud.spanner.connection.ConnectionOptions; import com.google.cloud.spanner.testing.EmulatorSpannerHelper; import com.google.common.base.Joiner; @@ -423,6 +410,22 @@ public void bindDateNull() { assertThat(row.isNull(0)).isTrue(); } + @Test + public void bindInterval() { + Interval d = Interval.parseFromString("P1Y2M3DT4H5M6.789123S"); + Struct row = execute(Statement.newBuilder(selectValueQuery).bind("p1").to(d), Type.interval()); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getInterval(0)).isEqualTo(d); + } + + @Test + public void bindIntervalNull() { + Struct row = + execute( + Statement.newBuilder(selectValueQuery).bind("p1").to((Interval) null), Type.interval()); + assertThat(row.isNull(0)).isTrue(); + } + @Test public void bindNumeric() { assumeFalse("Emulator does not yet support NUMERIC", EmulatorSpannerHelper.isUsingEmulator()); @@ -817,6 +820,39 @@ public void bindDateArrayNull() { assertThat(row.isNull(0)).isTrue(); } + @Test + public void bindIntervalArray() { + Interval d1 = Interval.parseFromString("P-1Y-2M-3DT4H5M6.789123S"); + Interval d2 = Interval.parseFromString("P1Y2M3DT-4H-5M-6.789123S"); + Struct row = + execute( + Statement.newBuilder(selectValueQuery).bind("p1").toIntervalArray(asList(d1, d2, null)), + Type.array(Type.interval())); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getIntervalList(0)).containsExactly(d1, d2, null).inOrder(); + } + + @Test + public void bindIntervalArrayEmpty() { + Struct row = + execute( + Statement.newBuilder(selectValueQuery) + .bind("p1") + .toIntervalArray(Collections.emptyList()), + Type.array(Type.interval())); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getIntervalList(0)).containsExactly(); + } + + @Test + public void bindIntervalArrayNull() { + Struct row = + execute( + Statement.newBuilder(selectValueQuery).bind("p1").toIntervalArray(null), + Type.array(Type.interval())); + assertThat(row.isNull(0)).isTrue(); + } + @Test public void bindNumericArrayGoogleStandardSQL() { assumeTrue(dialect.dialect == Dialect.GOOGLE_STANDARD_SQL); @@ -981,6 +1017,8 @@ private Struct structValue() { .to(Date.fromYearMonthDay(1, 3, 1)) .set("f_string") .to("hello") + .set("f_interval") + .to(Interval.fromMonthsDaysMicros(100, 200, 5000)) .set("f_bytes") .to(ByteArray.copyFrom("bytes")) .build(); @@ -998,6 +1036,7 @@ public void bindStruct() { + "@p.f_timestamp," + "@p.f_date," + "@p.f_string," + + "@p.f_interval," + "@p.f_bytes"; Struct row =