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

Add support for struct-typed parameters in Cloud Spanner. #3287

Merged
merged 1 commit into from
Jun 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,12 @@ public int getColumnIndex(String columnName) {
return getType().getFieldIndex(columnName);
}

protected void checkNonNull(int columnIndex, Object columnNameForError) {
if (isNull(columnIndex)) {
throw new NullPointerException("Column " + columnNameForError + " contains NULL value");
}
}

private void checkNonNullOfType(int columnIndex, Type expectedType, Object columnNameForError) {
Type actualType = getColumnType(columnIndex);
checkState(
Expand All @@ -353,9 +359,4 @@ private void checkNonNullArrayOfStruct(int columnIndex, Object columnNameForErro
checkNonNull(columnIndex, columnNameForError);
}

private void checkNonNull(int columnIndex, Object columnNameForError) {
if (isNull(columnIndex)) {
throw new NullPointerException("Column " + columnNameForError + " contains NULL value");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import com.google.cloud.ByteArray;
import com.google.cloud.Date;
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Type.Code;
import com.google.cloud.spanner.Type.StructField;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.spanner.v1.ResultSetStats;
Expand Down Expand Up @@ -49,6 +51,12 @@ private static class PrePopulatedResultSet implements ResultSet {
Preconditions.checkNotNull(rows);
Preconditions.checkNotNull(type);
Preconditions.checkArgument(type.getCode() == Type.Code.STRUCT);
for (StructField field : type.getStructFields()) {
if (field.getType().getCode() == Code.STRUCT) {
throw new UnsupportedOperationException(
"STRUCT-typed columns are not supported inside ResultSets.");
}
}
this.type = type;
this.rows = rows instanceof List<?> ? (List<Struct>) rows : Lists.newArrayList(rows);
for (Struct row : rows) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1973,14 +1973,22 @@ private Object writeReplace() {
builder.set(fieldName).toDateArray((Iterable<Date>) value);
break;
case STRUCT:
builder.add(fieldName, fieldType.getArrayElementType().getStructFields(), (Iterable<Struct>) value);
builder
.set(fieldName)
.toStructArray(fieldType.getArrayElementType(), (Iterable<Struct>) value);
break;
default:
throw new AssertionError(
"Unhandled array type code: " + fieldType.getArrayElementType());
}
break;
case STRUCT: // Not a legal top-level field type.
case STRUCT:
if (value == null) {
builder.set(fieldName).to(fieldType, null);
} else {
builder.set(fieldName).to((Struct) value);
}
break;
default:
throw new AssertionError("Unhandled type code: " + fieldType.getCode());
}
Expand All @@ -1994,6 +2002,11 @@ private Object writeReplace() {
this.rowData = rowData;
}

@Override
public String toString() {
return this.rowData.toString();
}

boolean consumeRow(Iterator<com.google.protobuf.Value> iterator) {
rowData.clear();
if (!iterator.hasNext()) {
Expand Down Expand Up @@ -2040,12 +2053,28 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot
checkType(fieldType, proto, KindCase.LIST_VALUE);
ListValue listValue = proto.getListValue();
return decodeArrayValue(fieldType.getArrayElementType(), listValue);
case STRUCT: // Not a legal top-level field type.
case STRUCT:
checkType(fieldType, proto, KindCase.LIST_VALUE);
ListValue structValue = proto.getListValue();
return decodeStructValue(fieldType, structValue);
default:
throw new AssertionError("Unhandled type code: " + fieldType.getCode());
}
}

private static Struct decodeStructValue(Type structType, ListValue structValue) {
List<Type.StructField> fieldTypes = structType.getStructFields();
checkArgument(
structValue.getValuesCount() == fieldTypes.size(),
"Size mismatch between type descriptor and actual values.");
List<Object> fields = new ArrayList<>(fieldTypes.size());
List<com.google.protobuf.Value> fieldValues = structValue.getValuesList();
for (int i = 0; i < fieldTypes.size(); ++i) {
fields.add(decodeValue(fieldTypes.get(i).getType(), fieldValues.get(i)));
}
return new GrpcStruct(structType, fields);
}

private static Object decodeArrayValue(Type elementType, ListValue listValue) {
switch (elementType.getCode()) {
case BOOL:
Expand Down Expand Up @@ -2117,16 +2146,8 @@ public String apply(com.google.protobuf.Value input) {
if (value.getKindCase() == KindCase.NULL_VALUE) {
list.add(null);
} else {
List<Type.StructField> fieldTypes = elementType.getStructFields();
List<Object> fields = new ArrayList<>(fieldTypes.size());
ListValue structValues = value.getListValue();
checkArgument(
structValues.getValuesCount() == fieldTypes.size(),
"Size mismatch between type descriptor and actual values.");
for (int i = 0; i < fieldTypes.size(); ++i) {
fields.add(decodeValue(fieldTypes.get(i).getType(), structValues.getValues(i)));
}
list.add(new GrpcStruct(elementType, fields));
ListValue structValue = value.getListValue();
list.add(decodeStructValue(elementType, structValue));
}
}
return list;
Expand Down Expand Up @@ -2199,6 +2220,11 @@ protected Date getDateInternal(int columnIndex) {
return (Date) rowData.get(columnIndex);
}

@Override
protected Struct getStructInternal(int columnIndex) {
return (Struct) rowData.get(columnIndex);
}

@Override
protected boolean[] getBooleanArrayInternal(int columnIndex) {
@SuppressWarnings("unchecked") // We know ARRAY<BOOL> produces a List<Boolean>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,53 @@
import com.google.cloud.ByteArray;
import com.google.cloud.Date;
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Type.Code;
import com.google.cloud.spanner.Type.StructField;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Booleans;
import com.google.common.primitives.Doubles;
import com.google.common.primitives.Longs;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

/**
* Represents a value of {@link Type.Code#STRUCT}. Such values are a tuple of named and typed
* columns, where individual columns may be null. Individual rows from a read or query operation can
* be considered as structs; {@link ResultSet#getCurrentRowAsStruct()} allows an immutable struct to
* be created from the row that the result set is currently positioned over.
* Represents a non-{@code NULL} value of {@link Type.Code#STRUCT}. Such values are a tuple of named
* and typed columns, where individual columns may be null. Individual rows from a read or query
* operation can be considered as structs; {@link ResultSet#getCurrentRowAsStruct()} allows an
* immutable struct to be created from the row that the result set is currently positioned over.
*
* <p>{@code Struct} instances are immutable.
*
* <p>This class does not support representing typed {@code NULL} {@code Struct} values.
*
* <p>However, struct values <em>inside</em> SQL queries are always typed and can be externally
* supplied to a query only in the form of struct/array-of-struct query parameter values for which
* typed {@code NULL} struct values can be specified in the following ways:
*
* <p>1. As a standalone {@code NULL} struct value or as a nested struct field value, constructed
* using {@link ValueBinder#to(Type, Struct)} or {@link Value#struct(Type, Struct)}.
*
* <p>2. As as a null {@code Struct} reference representing a {@code NULL} struct typed element
* value inside an array/list of '{@code Struct}' references, that is used to construct an
* array-of-struct value using {@link Value#structArray(Type, Iterable)} or {@link
* ValueBinder#toStructArray(Type, Iterable)}. In this case, the type of the {@code NULL} struct
* value is assumed to be the same as the explicitly specified struct element type of the
* array/list.
*/
@Immutable
public abstract class Struct extends AbstractStructReader implements Serializable {
// Only implementations within the package are allowed.
Struct() {}

/**
* Returns a builder for creating a {@code Struct} instance. This is intended mainly for test
* purposes: the library implementation is typically responsible for creating {@code Struct}
* instances.
*/
/** Returns a builder for creating a non-{@code NULL} {@code Struct} instance. */
public static Builder newBuilder() {
return new Builder();
}

/** Builder for {@code Struct} instances. */
/** Builder for constructing non-{@code NULL} {@code Struct} instances. */
public static final class Builder {
private final List<Type.StructField> types = new ArrayList<>();
private final List<Value> values = new ArrayList<>();
Expand All @@ -76,32 +88,25 @@ Builder handle(Value value) {
};
}

/** Returns a binder to set the value of a new field in the struct named {@code fieldName}. */
/**
* Returns a binder to set the value of a new field in the struct named {@code fieldName}.
*
* @param fieldName name of the field to set. Can be empty or the same as an existing field name
* in the {@code STRUCT}
*/
public ValueBinder<Builder> set(String fieldName) {
checkBindingInProgress(false);
currentField = checkNotNull(fieldName);
return binder;
}

/** Adds a new field named {@code fieldName} with the given value. */
public Builder add(String fieldName, Value value) {
/** Adds a new unnamed field {@code fieldName} with the given value. */
public Builder add(Value value) {
checkBindingInProgress(false);
addInternal(fieldName, value);
addInternal("", value);
return this;
}

/**
* Adds a new field of type {@code ARRAY<STRUCT<fieldTypes>>} named {@code fieldName} with the
* given element values. {@code elements} may be null, as may any member of {@code elements}.
* All non-null members of {@code elements} must be of type {@code STRUCT<fieldTypes>}
*/
public Builder add(
String fieldName,
Iterable<Type.StructField> fieldTypes,
@Nullable Iterable<Struct> elements) {
return add(fieldName, Value.structArray(fieldTypes, elements));
}

public Struct build() {
checkBindingInProgress(false);
return new ValueListStruct(types, values);
Expand All @@ -121,12 +126,42 @@ private void checkBindingInProgress(boolean expectInProgress) {
}
}

/** Default implementation for test value structs produced by {@link Builder}. */
/**
* TODO(user) : Consider moving these methods to the StructReader interface once STRUCT-typed
* columns are supported in {@link ResultSet}.
*/

/* Public methods for accessing struct-typed fields */
public Struct getStruct(int columnIndex) {
checkNonNullStruct(columnIndex, columnIndex);
return getStructInternal(columnIndex);
}

public Struct getStruct(String columnName) {
int columnIndex = getColumnIndex(columnName);
checkNonNullStruct(columnIndex, columnName);
return getStructInternal(columnIndex);
}

/* Sub-classes must implement this method */
protected abstract Struct getStructInternal(int columnIndex);

private void checkNonNullStruct(int columnIndex, Object columnNameForError) {
Type actualType = getColumnType(columnIndex);
checkState(
actualType.getCode() == Code.STRUCT,
"Column %s is not of correct type: expected STRUCT<...> but was %s",
columnNameForError,
actualType);
checkNonNull(columnIndex, columnNameForError);
}

/** Default implementation for value structs produced by {@link Builder}. */
private static class ValueListStruct extends Struct {
private final Type type;
private final List<Value> values;

private ValueListStruct(List<Type.StructField> types, List<Value> values) {
private ValueListStruct(Iterable<StructField> types, Iterable<Value> values) {
this.type = Type.struct(types);
this.values = ImmutableList.copyOf(values);
}
Expand Down Expand Up @@ -166,6 +201,11 @@ protected Date getDateInternal(int columnIndex) {
return values.get(columnIndex).getDate();
}

@Override
protected Struct getStructInternal(int columnIndex) {
return values.get(columnIndex).getStruct();
}

@Override
protected boolean[] getBooleanArrayInternal(int columnIndex) {
return Booleans.toArray(getBooleanListInternal(columnIndex));
Expand Down Expand Up @@ -289,6 +329,8 @@ private Object getAsObject(int columnIndex) {
return getTimestampInternal(columnIndex);
case DATE:
return getDateInternal(columnIndex);
case STRUCT:
return getStructInternal(columnIndex);
case ARRAY:
switch (type.getArrayElementType().getCode()) {
case BOOL:
Expand Down
Loading