Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Range object to allow reading range value #3236

Merged
merged 9 commits into from
Apr 17, 2024
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,20 @@ If you are using Maven without the BOM, add this to your dependencies:
If you are using Gradle 5.x or later, add this to your dependencies:

```Groovy
implementation platform('com.google.cloud:libraries-bom:26.34.0')
implementation platform('com.google.cloud:libraries-bom:26.37.0')

implementation 'com.google.cloud:google-cloud-bigquery'
```
If you are using Gradle without BOM, add this to your dependencies:

```Groovy
implementation 'com.google.cloud:google-cloud-bigquery:2.38.1'
implementation 'com.google.cloud:google-cloud-bigquery:2.38.2'
```

If you are using SBT, add this to your dependencies:

```Scala
libraryDependencies += "com.google.cloud" % "google-cloud-bigquery" % "2.38.1"
libraryDependencies += "com.google.cloud" % "google-cloud-bigquery" % "2.38.2"
```
<!-- {x-version-update-end} -->

Expand Down Expand Up @@ -351,7 +351,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
[kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-bigquery/java11.html
[stability-image]: https://img.shields.io/badge/stability-stable-green
[maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-bigquery.svg
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigquery/2.38.1
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigquery/2.38.2
[authentication]: https://github.com/googleapis/google-cloud-java#authentication
[auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
[predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,19 @@ public BigDecimal getNumericValue() {
return new BigDecimal(getStringValue());
}

/**
* Returns this field's value as a {@link Range}. This method should only be used * if the
* corresponding field has {@link LegacySQLTypeName#RANGE} type.
*
* @throws ClassCastException if the field is not a primitive type
* @throws IllegalArgumentException if the field's value could not be converted to {@link Range}
* @throws NullPointerException if {@link #isNull()} returns {@code true}
*/
@SuppressWarnings("unchecked")
public Range getRangeValue() {
PhongChuong marked this conversation as resolved.
Show resolved Hide resolved
return Range.of(getStringValue());
}

/**
* Returns this field's value as a list of {@link FieldValue}. This method should only be used if
* the corresponding field has {@link Field.Mode#REPEATED} mode (i.e. {@link #getAttribute()} is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static org.threeten.bp.temporal.ChronoField.SECOND_OF_MINUTE;

import com.google.api.services.bigquery.model.QueryParameterType;
import com.google.api.services.bigquery.model.RangeValue;
import com.google.auto.value.AutoValue;
import com.google.cloud.Timestamp;
import com.google.common.base.Function;
Expand Down Expand Up @@ -141,6 +142,13 @@ public Builder setStructValues(Map<String, QueryParameterValue> structValues) {

abstract Builder setStructValuesInner(Map<String, QueryParameterValue> structValues);

/** Sets range values. The type must set to RANGE. */
public Builder setRangeValues(Range range) {
return setRangeValuesInner(range);
}

abstract Builder setRangeValuesInner(Range range);

/** Sets the parameter data type. */
public abstract Builder setType(StandardSQLTypeName type);

Expand Down Expand Up @@ -184,6 +192,15 @@ public Map<String, QueryParameterValue> getStructValues() {
@Nullable
abstract Map<String, QueryParameterValue> getStructValuesInner();

/** Returns the struct values of this parameter. The returned map, if not null, is immutable. */
@Nullable
public Range getRangeValues() {
return getRangeValuesInner();
}

@Nullable
abstract Range getRangeValuesInner();

/** Returns the data type of this parameter. */
public abstract StandardSQLTypeName getType();

Expand Down Expand Up @@ -333,6 +350,14 @@ public static QueryParameterValue interval(PeriodDuration value) {
return of(value, StandardSQLTypeName.INTERVAL);
}

/** Creates a {@code QueryParameterValue} object with a type of RANGE. */
public static QueryParameterValue range(Range value) {
return QueryParameterValue.newBuilder()
.setRangeValues(value)
.setType(StandardSQLTypeName.RANGE)
.build();
}

/**
* Creates a {@code QueryParameterValue} object with a type of ARRAY, and an array element type
* based on the given class.
Expand Down Expand Up @@ -442,6 +467,8 @@ private static <T> String valueToStringOrNull(T value, StandardSQLTypeName type)
throw new IllegalArgumentException("Cannot convert STRUCT to String value");
case ARRAY:
throw new IllegalArgumentException("Cannot convert ARRAY to String value");
case RANGE:
throw new IllegalArgumentException("Cannot convert RANGE to String value");
case TIMESTAMP:
if (value instanceof Long) {
Timestamp timestamp = Timestamp.ofTimeMicroseconds((Long) value);
Expand Down Expand Up @@ -517,6 +544,22 @@ com.google.api.services.bigquery.model.QueryParameterValue toValuePb() {
}
valuePb.setStructValues(structValues);
}
if (getType() == StandardSQLTypeName.RANGE) {
RangeValue rangeValue = new RangeValue();
if (!getRangeValues().getStart().isNull()) {
com.google.api.services.bigquery.model.QueryParameterValue startValue =
new com.google.api.services.bigquery.model.QueryParameterValue();
startValue.setValue(getRangeValues().getStart().getStringValue());
rangeValue.setStart(startValue);
}
if (!getRangeValues().getEnd().isNull()) {
com.google.api.services.bigquery.model.QueryParameterValue endValue =
new com.google.api.services.bigquery.model.QueryParameterValue();
endValue.setValue(getRangeValues().getEnd().getStringValue());
rangeValue.setEnd(endValue);
}
valuePb.setRangeValue(rangeValue);
}
return valuePb;
}

Expand Down Expand Up @@ -544,6 +587,13 @@ QueryParameterType toTypePb() {
}
typePb.setStructTypes(structTypes);
}
if (getType() == StandardSQLTypeName.RANGE
&& getRangeValues() != null
&& getRangeValues().getType() != null) {
QueryParameterType rangeTypePb = new QueryParameterType();
rangeTypePb.setType(getRangeValues().getType().toString());
typePb.setRangeElementType(rangeTypePb);
}
return typePb;
}

Expand Down Expand Up @@ -592,6 +642,21 @@ static QueryParameterValue fromPb(
}
valueBuilder.setStructValues(structValues);
}
} else if (type == StandardSQLTypeName.RANGE) {
Range.Builder range = Range.newBuilder();
if (valuePb.getRangeValue() != null) {
com.google.api.services.bigquery.model.RangeValue rangeValuePb = valuePb.getRangeValue();
if (rangeValuePb.getStart() != null && rangeValuePb.getStart().getValue() != null) {
range.setStart(valuePb.getRangeValue().getStart().getValue());
}
if (rangeValuePb.getEnd() != null && rangeValuePb.getEnd().getValue() != null) {
range.setEnd(valuePb.getRangeValue().getEnd().getValue());
}
}
if (typePb.getRangeElementType() != null && typePb.getRangeElementType().getType() != null) {
range.setType(StandardSQLTypeName.valueOf(typePb.getRangeElementType().getType()));
}
valueBuilder.setRangeValues(range.build());
} else {
valueBuilder.setValue(valuePb == null ? "" : valuePb.getValue());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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.bigquery;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.auto.value.AutoValue;
import com.google.cloud.bigquery.FieldValue.Attribute;
import java.io.Serializable;
import javax.annotation.Nullable;

@AutoValue
public abstract class Range implements Serializable {
private static final long serialVersionUID = 1L;

/** Returns the start value of the range. A null value represents an unbounded start. */
public FieldValue getStart() {
// The supported Range types [DATE, TIME, TIMESTAMP] are all Attribute.PRIMITIVE.
return FieldValue.of(Attribute.PRIMITIVE, getStartInner());
}

@Nullable
abstract String getStartInner();

/** Returns the end value of the range. A null value represents an unbounded end. */
public FieldValue getEnd() {
// The supported Range types [DATE, TIME, TIMESTAMP] are all Attribute.PRIMITIVE.
return FieldValue.of(Attribute.PRIMITIVE, getEndInner());
}

@Nullable
abstract String getEndInner();

@Nullable
/** Returns the type of the range. */
public abstract StandardSQLTypeName getType();

public abstract Range.Builder toBuilder();

@AutoValue.Builder
public abstract static class Builder {

public Range.Builder setStart(String start) {
return setStartInner(start);
}

abstract Range.Builder setStartInner(String start);

public Range.Builder setEnd(String end) {
return setEndInner(end);
}

abstract Range.Builder setEndInner(String end);

public abstract Range.Builder setType(StandardSQLTypeName type);

public abstract Range build();
}

/** Creates a range builder. Supported StandardSQLTypeName are [DATE, DATETIME, TIMESTAMP] */
public static Builder newBuilder() {
return new AutoValue_Range.Builder();
}

/**
* Creates an instance of {@code Range} from a string representation.
*
* <p>The expected string format is: "[start, end)", where start and end are string format of
* [DATE, TIME, TIMESTAMP].
*/
public static Range of(String value) throws IllegalArgumentException {
PhongChuong marked this conversation as resolved.
Show resolved Hide resolved
checkNotNull(value);
Range.Builder builder = newBuilder();
String[] startEnd = value.split(", ", 2); // Expect an extra space after ','.
if (startEnd.length != 2) {
throw new IllegalArgumentException(
String.format("Expected Range value string to be [start, end) and got %s", value));
}

String start = startEnd[0].substring(1); // Ignore the [
String end = startEnd[1].substring(0, startEnd[1].length() - 1); // Ignore the )
if (start.equalsIgnoreCase("UNBOUNDED") || (start.equalsIgnoreCase("NULL"))) {
builder.setStart(null);
} else {
builder.setStart(start);
}
if (end.equalsIgnoreCase("UNBOUNDED") || (end.equalsIgnoreCase("NULL"))) {
builder.setEnd(null);
} else {
builder.setEnd(end);
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public class FieldValueTest {
private static final Map<String, String> BYTES_FIELD = ImmutableMap.of("v", BYTES_BASE64);
private static final Map<String, String> NULL_FIELD =
ImmutableMap.of("v", Data.nullOf(String.class));

private static final Map<String, String> RANGE_FIELD = ImmutableMap.of("v", "[start, end)");

private static final Map<String, Object> REPEATED_FIELD =
ImmutableMap.<String, Object>of("v", ImmutableList.<Object>of(INTEGER_FIELD, INTEGER_FIELD));
private static final Map<String, Object> RECORD_FIELD =
Expand Down Expand Up @@ -99,6 +102,9 @@ public void testFromPb() {
assertArrayEquals(BYTES, value.getBytesValue());
value = FieldValue.fromPb(NULL_FIELD);
assertNull(value.getValue());
value = FieldValue.fromPb(RANGE_FIELD);
assertEquals(FieldValue.Attribute.PRIMITIVE, value.getAttribute());
assertEquals(Range.of(RANGE_FIELD.get("v")), value.getRangeValue());
value = FieldValue.fromPb(REPEATED_FIELD);
assertEquals(FieldValue.Attribute.REPEATED, value.getAttribute());
assertEquals(FieldValue.fromPb(INTEGER_FIELD), value.getRepeatedValue().get(0));
Expand Down Expand Up @@ -156,6 +162,10 @@ public void testEquals() {
assertEquals(nullValue, FieldValue.fromPb(NULL_FIELD));
assertEquals(nullValue.hashCode(), FieldValue.fromPb(NULL_FIELD).hashCode());

FieldValue rangeValue = FieldValue.of(FieldValue.Attribute.PRIMITIVE, "[start, end)");
assertEquals(rangeValue, FieldValue.fromPb(RANGE_FIELD));
assertEquals(rangeValue.hashCode(), FieldValue.fromPb(RANGE_FIELD).hashCode());

FieldValue repeatedValue =
FieldValue.of(FieldValue.Attribute.REPEATED, ImmutableList.of(integerValue, integerValue));
assertEquals(repeatedValue, FieldValue.fromPb(REPEATED_FIELD));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -619,4 +619,54 @@ private static void assertArrayDataEquals(
assertThat(value.getArrayValues()).isNull();
}
}

@Test
public void testRange() {
testRangeDataEquals(null, null, StandardSQLTypeName.DATE);
testRangeDataEquals(null, "1971-02-03", StandardSQLTypeName.DATE);
testRangeDataEquals("1970-01-02", null, StandardSQLTypeName.DATE);
testRangeDataEquals("1970-01-02", "1971-02-03", StandardSQLTypeName.DATE);

testRangeDataEquals(null, null, StandardSQLTypeName.DATETIME);
testRangeDataEquals(null, "2015-09-20 06:41:35.220000", StandardSQLTypeName.DATETIME);
testRangeDataEquals("2014-08-19 05:41:35.220000", null, StandardSQLTypeName.DATETIME);
testRangeDataEquals(
"2014-08-19 05:41:35.220000", "2015-09-20 06:41:35.220000", StandardSQLTypeName.DATETIME);

testRangeDataEquals(null, null, StandardSQLTypeName.TIMESTAMP);
testRangeDataEquals(null, "2015-09-20 13:41:35.220000+01:00", StandardSQLTypeName.TIMESTAMP);
testRangeDataEquals("2014-08-19 12:41:35.220000+00:00", null, StandardSQLTypeName.TIMESTAMP);
testRangeDataEquals(
"2014-08-19 12:41:35.220000+00:00",
"2015-09-20 13:41:35.220000+01:00",
StandardSQLTypeName.TIMESTAMP);
}

/** Helper method to test range QueryParameterValue and its permutations. */
private static void testRangeDataEquals(String start, String end, StandardSQLTypeName type) {
QueryParameterValue rangeField =
QueryParameterValue.range(
Range.newBuilder().setType(type).setStart(start).setEnd(end).build());
QueryParameterType parameterType = rangeField.toTypePb();
com.google.api.services.bigquery.model.QueryParameterValue parameterValue =
rangeField.toValuePb();
QueryParameterValue queryParameterValue =
QueryParameterValue.fromPb(parameterValue, parameterType);

assertThat(queryParameterValue.getType()).isEqualTo(StandardSQLTypeName.RANGE);
if (start == null) {
assertThat(queryParameterValue.getRangeValues().getStart().isNull()).isTrue();
} else {
assertThat(queryParameterValue.getRangeValues().getStart().getStringValue()).isEqualTo(start);
}
if (end == null) {
assertThat(queryParameterValue.getRangeValues().getEnd().isNull()).isTrue();
} else {
assertThat(queryParameterValue.getRangeValues().getEnd().getStringValue()).isEqualTo(end);
}
assertThat(queryParameterValue.getRangeValues().getType()).isEqualTo(type);
assertThat(queryParameterValue.getArrayValues()).isNull();
assertThat(queryParameterValue.getStructValues()).isNull();
assertThat(queryParameterValue.getValue()).isNull();
}
}