Skip to content

Commit 5750ffd

Browse files
committed
Add support for struct-typed parameters.
1 parent 5c4c5bc commit 5750ffd

File tree

12 files changed

+1133
-238
lines changed

12 files changed

+1133
-238
lines changed

google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,12 @@ public int getColumnIndex(String columnName) {
331331
return getType().getFieldIndex(columnName);
332332
}
333333

334+
protected void checkNonNull(int columnIndex, Object columnNameForError) {
335+
if (isNull(columnIndex)) {
336+
throw new NullPointerException("Column " + columnNameForError + " contains NULL value");
337+
}
338+
}
339+
334340
private void checkNonNullOfType(int columnIndex, Type expectedType, Object columnNameForError) {
335341
Type actualType = getColumnType(columnIndex);
336342
checkState(
@@ -353,9 +359,4 @@ private void checkNonNullArrayOfStruct(int columnIndex, Object columnNameForErro
353359
checkNonNull(columnIndex, columnNameForError);
354360
}
355361

356-
private void checkNonNull(int columnIndex, Object columnNameForError) {
357-
if (isNull(columnIndex)) {
358-
throw new NullPointerException("Column " + columnNameForError + " contains NULL value");
359-
}
360-
}
361362
}

google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import com.google.cloud.ByteArray;
2020
import com.google.cloud.Date;
2121
import com.google.cloud.Timestamp;
22+
import com.google.cloud.spanner.Type.Code;
23+
import com.google.cloud.spanner.Type.StructField;
2224
import com.google.common.base.Preconditions;
2325
import com.google.common.collect.Lists;
2426
import com.google.spanner.v1.ResultSetStats;
@@ -49,6 +51,12 @@ private static class PrePopulatedResultSet implements ResultSet {
4951
Preconditions.checkNotNull(rows);
5052
Preconditions.checkNotNull(type);
5153
Preconditions.checkArgument(type.getCode() == Type.Code.STRUCT);
54+
for (StructField field : type.getStructFields()) {
55+
if (field.getType().getCode() == Code.STRUCT) {
56+
throw new UnsupportedOperationException(
57+
"STRUCT-typed columns are not supported inside ResultSets.");
58+
}
59+
}
5260
this.type = type;
5361
this.rows = rows instanceof List<?> ? (List<Struct>) rows : Lists.newArrayList(rows);
5462
for (Struct row : rows) {

google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java

+39-13
Original file line numberDiff line numberDiff line change
@@ -1973,14 +1973,22 @@ private Object writeReplace() {
19731973
builder.set(fieldName).toDateArray((Iterable<Date>) value);
19741974
break;
19751975
case STRUCT:
1976-
builder.add(fieldName, fieldType.getArrayElementType().getStructFields(), (Iterable<Struct>) value);
1976+
builder
1977+
.set(fieldName)
1978+
.toStructArray(fieldType.getArrayElementType(), (Iterable<Struct>) value);
19771979
break;
19781980
default:
19791981
throw new AssertionError(
19801982
"Unhandled array type code: " + fieldType.getArrayElementType());
19811983
}
19821984
break;
1983-
case STRUCT: // Not a legal top-level field type.
1985+
case STRUCT:
1986+
if (value == null) {
1987+
builder.set(fieldName).to(fieldType, null);
1988+
} else {
1989+
builder.set(fieldName).to((Struct) value);
1990+
}
1991+
break;
19841992
default:
19851993
throw new AssertionError("Unhandled type code: " + fieldType.getCode());
19861994
}
@@ -1994,6 +2002,11 @@ private Object writeReplace() {
19942002
this.rowData = rowData;
19952003
}
19962004

2005+
@Override
2006+
public String toString() {
2007+
return this.rowData.toString();
2008+
}
2009+
19972010
boolean consumeRow(Iterator<com.google.protobuf.Value> iterator) {
19982011
rowData.clear();
19992012
if (!iterator.hasNext()) {
@@ -2040,12 +2053,28 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot
20402053
checkType(fieldType, proto, KindCase.LIST_VALUE);
20412054
ListValue listValue = proto.getListValue();
20422055
return decodeArrayValue(fieldType.getArrayElementType(), listValue);
2043-
case STRUCT: // Not a legal top-level field type.
2056+
case STRUCT:
2057+
checkType(fieldType, proto, KindCase.LIST_VALUE);
2058+
ListValue structValue = proto.getListValue();
2059+
return decodeStructValue(fieldType, structValue);
20442060
default:
20452061
throw new AssertionError("Unhandled type code: " + fieldType.getCode());
20462062
}
20472063
}
20482064

2065+
private static Struct decodeStructValue(Type structType, ListValue structValue) {
2066+
List<Type.StructField> fieldTypes = structType.getStructFields();
2067+
checkArgument(
2068+
structValue.getValuesCount() == fieldTypes.size(),
2069+
"Size mismatch between type descriptor and actual values.");
2070+
List<Object> fields = new ArrayList<>(fieldTypes.size());
2071+
List<com.google.protobuf.Value> fieldValues = structValue.getValuesList();
2072+
for (int i = 0; i < fieldTypes.size(); ++i) {
2073+
fields.add(decodeValue(fieldTypes.get(i).getType(), fieldValues.get(i)));
2074+
}
2075+
return new GrpcStruct(structType, fields);
2076+
}
2077+
20492078
private static Object decodeArrayValue(Type elementType, ListValue listValue) {
20502079
switch (elementType.getCode()) {
20512080
case BOOL:
@@ -2117,16 +2146,8 @@ public String apply(com.google.protobuf.Value input) {
21172146
if (value.getKindCase() == KindCase.NULL_VALUE) {
21182147
list.add(null);
21192148
} else {
2120-
List<Type.StructField> fieldTypes = elementType.getStructFields();
2121-
List<Object> fields = new ArrayList<>(fieldTypes.size());
2122-
ListValue structValues = value.getListValue();
2123-
checkArgument(
2124-
structValues.getValuesCount() == fieldTypes.size(),
2125-
"Size mismatch between type descriptor and actual values.");
2126-
for (int i = 0; i < fieldTypes.size(); ++i) {
2127-
fields.add(decodeValue(fieldTypes.get(i).getType(), structValues.getValues(i)));
2128-
}
2129-
list.add(new GrpcStruct(elementType, fields));
2149+
ListValue structValue = value.getListValue();
2150+
list.add(decodeStructValue(elementType, structValue));
21302151
}
21312152
}
21322153
return list;
@@ -2199,6 +2220,11 @@ protected Date getDateInternal(int columnIndex) {
21992220
return (Date) rowData.get(columnIndex);
22002221
}
22012222

2223+
@Override
2224+
protected Struct getStructInternal(int columnIndex) {
2225+
return (Struct) rowData.get(columnIndex);
2226+
}
2227+
22022228
@Override
22032229
protected boolean[] getBooleanArrayInternal(int columnIndex) {
22042230
@SuppressWarnings("unchecked") // We know ARRAY<BOOL> produces a List<Boolean>.

google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java

+72-30
Original file line numberDiff line numberDiff line change
@@ -22,41 +22,53 @@
2222
import com.google.cloud.ByteArray;
2323
import com.google.cloud.Date;
2424
import com.google.cloud.Timestamp;
25+
import com.google.cloud.spanner.Type.Code;
26+
import com.google.cloud.spanner.Type.StructField;
2527
import com.google.common.collect.ImmutableList;
2628
import com.google.common.primitives.Booleans;
2729
import com.google.common.primitives.Doubles;
2830
import com.google.common.primitives.Longs;
29-
3031
import java.io.Serializable;
3132
import java.util.ArrayList;
3233
import java.util.List;
3334
import java.util.Objects;
34-
import javax.annotation.Nullable;
3535
import javax.annotation.concurrent.Immutable;
3636

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

50-
/**
51-
* Returns a builder for creating a {@code Struct} instance. This is intended mainly for test
52-
* purposes: the library implementation is typically responsible for creating {@code Struct}
53-
* instances.
54-
*/
66+
/** Returns a builder for creating a non-{@code NULL} {@code Struct} instance. */
5567
public static Builder newBuilder() {
5668
return new Builder();
5769
}
5870

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

79-
/** Returns a binder to set the value of a new field in the struct named {@code fieldName}. */
91+
/**
92+
* Returns a binder to set the value of a new field in the struct named {@code fieldName}.
93+
*
94+
* @param fieldName name of the field to set. Can be empty or the same as an existing field name
95+
* in the {@code STRUCT}
96+
*/
8097
public ValueBinder<Builder> set(String fieldName) {
8198
checkBindingInProgress(false);
8299
currentField = checkNotNull(fieldName);
83100
return binder;
84101
}
85102

86-
/** Adds a new field named {@code fieldName} with the given value. */
87-
public Builder add(String fieldName, Value value) {
103+
/** Adds a new unnamed field {@code fieldName} with the given value. */
104+
public Builder add(Value value) {
88105
checkBindingInProgress(false);
89-
addInternal(fieldName, value);
106+
addInternal("", value);
90107
return this;
91108
}
92109

93-
/**
94-
* Adds a new field of type {@code ARRAY<STRUCT<fieldTypes>>} named {@code fieldName} with the
95-
* given element values. {@code elements} may be null, as may any member of {@code elements}.
96-
* All non-null members of {@code elements} must be of type {@code STRUCT<fieldTypes>}
97-
*/
98-
public Builder add(
99-
String fieldName,
100-
Iterable<Type.StructField> fieldTypes,
101-
@Nullable Iterable<Struct> elements) {
102-
return add(fieldName, Value.structArray(fieldTypes, elements));
103-
}
104-
105110
public Struct build() {
106111
checkBindingInProgress(false);
107112
return new ValueListStruct(types, values);
@@ -121,12 +126,42 @@ private void checkBindingInProgress(boolean expectInProgress) {
121126
}
122127
}
123128

124-
/** Default implementation for test value structs produced by {@link Builder}. */
129+
/**
130+
* TODO(user) : Consider moving these methods to the StructReader interface once STRUCT-typed
131+
* columns are supported in {@link ResultSet}.
132+
*/
133+
134+
/* Public methods for accessing struct-typed fields */
135+
public Struct getStruct(int columnIndex) {
136+
checkNonNullStruct(columnIndex, columnIndex);
137+
return getStructInternal(columnIndex);
138+
}
139+
140+
public Struct getStruct(String columnName) {
141+
int columnIndex = getColumnIndex(columnName);
142+
checkNonNullStruct(columnIndex, columnName);
143+
return getStructInternal(columnIndex);
144+
}
145+
146+
/* Sub-classes must implement this method */
147+
protected abstract Struct getStructInternal(int columnIndex);
148+
149+
private void checkNonNullStruct(int columnIndex, Object columnNameForError) {
150+
Type actualType = getColumnType(columnIndex);
151+
checkState(
152+
actualType.getCode() == Code.STRUCT,
153+
"Column %s is not of correct type: expected STRUCT<...> but was %s",
154+
columnNameForError,
155+
actualType);
156+
checkNonNull(columnIndex, columnNameForError);
157+
}
158+
159+
/** Default implementation for value structs produced by {@link Builder}. */
125160
private static class ValueListStruct extends Struct {
126161
private final Type type;
127162
private final List<Value> values;
128163

129-
private ValueListStruct(List<Type.StructField> types, List<Value> values) {
164+
private ValueListStruct(Iterable<StructField> types, Iterable<Value> values) {
130165
this.type = Type.struct(types);
131166
this.values = ImmutableList.copyOf(values);
132167
}
@@ -166,6 +201,11 @@ protected Date getDateInternal(int columnIndex) {
166201
return values.get(columnIndex).getDate();
167202
}
168203

204+
@Override
205+
protected Struct getStructInternal(int columnIndex) {
206+
return values.get(columnIndex).getStruct();
207+
}
208+
169209
@Override
170210
protected boolean[] getBooleanArrayInternal(int columnIndex) {
171211
return Booleans.toArray(getBooleanListInternal(columnIndex));
@@ -289,6 +329,8 @@ private Object getAsObject(int columnIndex) {
289329
return getTimestampInternal(columnIndex);
290330
case DATE:
291331
return getDateInternal(columnIndex);
332+
case STRUCT:
333+
return getStructInternal(columnIndex);
292334
case ARRAY:
293335
switch (type.getArrayElementType().getCode()) {
294336
case BOOL:

0 commit comments

Comments
 (0)