Skip to content

Commit

Permalink
feat: Add Range object to allow reading range value (#3236)
Browse files Browse the repository at this point in the history
* feat: Add Range object to allow reading range value

This PR also adds the ability to use Range query parameter

* fix: lint error

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* Add IT test for listTableData with Range value

* Change Range get[Start, End] to return FieldValue

* Fix QueryParameterValueTest

* Update FieldValue to include type for Range values

---------

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
  • Loading branch information
PhongChuong and gcf-owl-bot[bot] committed Apr 17, 2024
1 parent 975df05 commit 2c3399d
Show file tree
Hide file tree
Showing 9 changed files with 508 additions and 8 deletions.
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;
}
}
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() {
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
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
@@ -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();
}
}
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
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

0 comments on commit 2c3399d

Please sign in to comment.