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 @@ -15,6 +15,7 @@
*/
package com.google.cloud.bigquery;

import com.google.api.services.bigquery.model.QueryParameterType;
import com.google.api.services.bigquery.model.TableFieldSchema;
import com.google.auto.value.AutoValue;
import java.io.Serializable;
Expand Down Expand Up @@ -60,4 +61,14 @@ static FieldElementType fromPb(TableFieldSchema.RangeElementType rangeElementTyp
}
return null;
}

/** Creates an instance of FieldElementType from QueryParameterType with RangeElementType. */
static FieldElementType fromPb(QueryParameterType queryParameterTypePb) {
// Treat a FieldElementType message without a Type subfield as invalid.
if ((queryParameterTypePb.getRangeElementType() != null)
&& (queryParameterTypePb.getRangeElementType().getType() != null)) {
return newBuilder().setType(queryParameterTypePb.getRangeElementType().getType()).build();
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ public enum Attribute {
REPEATED,

/** A {@code FieldValue} for a field of type {@link LegacySQLTypeName#RECORD}. */
RECORD
RECORD,

/** A {@code FieldValue} for a field of type {@link LegacySQLTypeName#RANGE}. */
RANGE
}

private FieldValue(Attribute attribute, Object value) {
Expand Down Expand Up @@ -229,6 +232,23 @@ 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
if (attribute == Attribute.RANGE) {
return (Range) value;
}
// Provide best effort to convert value to Range object.
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 Expand Up @@ -332,6 +352,12 @@ static FieldValue fromPb(Object cellPb, Field recordSchema) {
return FieldValue.of(Attribute.PRIMITIVE, null);
}
if (cellPb instanceof String) {
if ((recordSchema != null)
&& (recordSchema.getType() == LegacySQLTypeName.RANGE)
&& (recordSchema.getRangeElementType() != null)) {
return FieldValue.of(
Attribute.RANGE, Range.of((String) cellPb, recordSchema.getRangeElementType()));
}
return FieldValue.of(Attribute.PRIMITIVE, cellPb);
}
if (cellPb instanceof List) {
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().getType());
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(FieldElementType.fromPb(typePb));
}
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,114 @@
/*
* 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();

/** Returns the type of the range. */
@Nullable
public abstract FieldElementType 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(FieldElementType type);

public abstract Range build();
}

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

public static Range of(String value) throws IllegalArgumentException {
return of(value, null);
}

/**
* 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, FieldElementType type) throws IllegalArgumentException {
checkNotNull(value);
Range.Builder builder = newBuilder();
if (type != null) {
builder.setType(type);
}
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 @@ -17,6 +17,7 @@

import static org.junit.Assert.assertEquals;

import com.google.api.services.bigquery.model.QueryParameterType;
import org.junit.Test;

public class FieldElementTypeTest {
Expand All @@ -36,6 +37,11 @@ public void testBuilder() {
@Test
public void testFromAndPb() {
assertEquals(FIELD_ELEMENT_TYPE, FieldElementType.fromPb(FIELD_ELEMENT_TYPE.toPb()));
assertEquals(
FIELD_ELEMENT_TYPE,
FieldElementType.fromPb(
new QueryParameterType()
.setRangeElementType(new QueryParameterType().setType("DATE"))));
}

private void compareFieldElementType(FieldElementType expected, FieldElementType value) {
Expand Down
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