Skip to content

Commit

Permalink
Add support for struct-typed parameters.
Browse files Browse the repository at this point in the history
  • Loading branch information
adirastogi committed May 25, 2018
1 parent 5c4c5bc commit 964c4f2
Show file tree
Hide file tree
Showing 12 changed files with 1,123 additions and 222 deletions.
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 @@ -233,7 +273,7 @@ public boolean isNull(int columnIndex) {

@Override
public String toString() {
// TODO(user): Consider pulling a generic toString() up to Struct.
// TODO(user): Consier pulling a generic toString() up to Struct.
return values.toString();
}
}
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

0 comments on commit 964c4f2

Please sign in to comment.