Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
import org.threeten.bp.Instant;
import org.threeten.bp.ZoneOffset;
Expand Down Expand Up @@ -349,7 +350,11 @@ public static <T> QueryParameterValue array(T[] array, Class<T> clazz) {
public static <T> QueryParameterValue array(T[] array, StandardSQLTypeName type) {
List<QueryParameterValue> listValues = new ArrayList<>();
for (T obj : array) {
listValues.add(QueryParameterValue.of(obj, type));
if (type == StandardSQLTypeName.STRUCT) {
listValues.add((QueryParameterValue) obj);
} else {
listValues.add(QueryParameterValue.of(obj, type));
}
}
return QueryParameterValue.newBuilder()
.setArrayValues(listValues)
Expand Down Expand Up @@ -522,9 +527,16 @@ QueryParameterType toTypePb() {
QueryParameterType typePb = new QueryParameterType();
typePb.setType(getType().toString());
if (getArrayType() != null) {
QueryParameterType arrayTypePb = new QueryParameterType();
arrayTypePb.setType(getArrayType().toString());
typePb.setArrayType(arrayTypePb);
if (getArrayType() == StandardSQLTypeName.STRUCT) {
List<QueryParameterValue> values =
Objects.requireNonNull(getArrayValues(), "Array of struct cannot be empty");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this restriction coming from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is currently no way for users to supply detailed struct types (including the fields) when creating an array of struct query parameter using the QueryParameterValue.array method, which would limit us to the option of peeking at the first record to figure out the struct fields.

Ideally, query parameter types should not be limited to primitives and StandardSQLTypeName, users should create more general/nested types and supply them to QueryParameterValue.array, but this would be a much bigger change.

I think this fix with the limitation would solve most/all use cases until the changes mentioned are made.

QueryParameterType structType = values.get(0).toTypePb();
typePb.setArrayType(structType);
} else {
QueryParameterType arrayTypePb = new QueryParameterType();
arrayTypePb.setType(getArrayType().toString());
typePb.setArrayType(arrayTypePb);
}
}
if (getStructTypes() != null) {
List<QueryParameterType.StructTypes> structTypes = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.math.BigDecimal;
import java.text.ParseException;
import java.time.Period;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -563,6 +564,48 @@ public void testNestedStruct() {
assertThat(nestedRecordField.getStructValues().size()).isEqualTo(structValue.size());
}

@Test
public void testStructArray() {
Boolean[] boolValues = new Boolean[] {true, false};
Integer[] intValues = new Integer[] {15, 20};
String[] stringValues = new String[] {"test-string", "test-string2"};
List<ImmutableMap<String, QueryParameterValue>> fieldMaps = new ArrayList<>();
List<QueryParameterValue> tuples = new ArrayList<>();
for (int i = 0; i < 2; i++) {
QueryParameterValue booleanField = QueryParameterValue.bool(boolValues[i]);
QueryParameterValue integerField = QueryParameterValue.int64(intValues[i]);
QueryParameterValue stringField = QueryParameterValue.string(stringValues[i]);
ImmutableMap<String, QueryParameterValue> fieldMap =
ImmutableMap.of(
"booleanField",
booleanField,
"integerField",
integerField,
"stringField",
stringField);
fieldMaps.add(fieldMap);
QueryParameterValue recordField = QueryParameterValue.struct(fieldMap);
tuples.add(recordField);
}
QueryParameterValue repeatedRecordField =
QueryParameterValue.array(tuples.toArray(), StandardSQLTypeName.STRUCT);
com.google.api.services.bigquery.model.QueryParameterValue parameterValue =
repeatedRecordField.toValuePb();
QueryParameterType parameterType = repeatedRecordField.toTypePb();
QueryParameterValue queryParameterValue =
QueryParameterValue.fromPb(parameterValue, parameterType);
assertThat(queryParameterValue.getValue()).isNull();
assertThat(queryParameterValue.getType()).isEqualTo(StandardSQLTypeName.ARRAY);
assertThat(queryParameterValue.getArrayType()).isEqualTo(StandardSQLTypeName.STRUCT);
assertThat(queryParameterValue.getArrayValues().size()).isEqualTo(2);
for (int i = 0; i < 2; i++) {
QueryParameterValue record = queryParameterValue.getArrayValues().get(i);
assertThat(record.getType()).isEqualTo(StandardSQLTypeName.STRUCT);
assertThat(record.getStructTypes()).isNotNull();
assertThat(record.getStructValues()).isEqualTo(fieldMaps.get(i));
}
}

private static void assertArrayDataEquals(
String[] expectedValues,
StandardSQLTypeName expectedType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4062,6 +4062,47 @@ public void testStructNamedQueryParameters() throws InterruptedException {
}
}

@Test
public void testRepeatedRecordNamedQueryParameters() throws InterruptedException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also write a test with UNNEST option, since that is the particular use case the customer is using?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, will work on it

String[] stringValues = new String[] {"test-stringField", "test-stringField2"};
List<QueryParameterValue> tuples = new ArrayList<>();
for (int i = 0; i < 2; i++) {
QueryParameterValue stringValue = QueryParameterValue.string(stringValues[i]);
Map<String, QueryParameterValue> struct = new HashMap<>();
struct.put("stringField", stringValue);
QueryParameterValue recordValue = QueryParameterValue.struct(struct);
tuples.add(recordValue);
}

QueryParameterValue repeatedRecord =
QueryParameterValue.array(tuples.toArray(), StandardSQLTypeName.STRUCT);
String query = "SELECT @repeatedRecordField AS repeatedRecord";
QueryJobConfiguration config =
QueryJobConfiguration.newBuilder(query)
.setDefaultDataset(DATASET)
.setUseLegacySql(false)
.addNamedParameter("repeatedRecordField", repeatedRecord)
.build();
TableResult result = bigquery.query(config);
assertEquals(1, Iterables.size(result.getValues()));

FieldList subSchema = result.getSchema().getFields().get("repeatedRecord").getSubFields();
for (FieldValueList values : result.iterateAll()) {
for (FieldValue value : values) {
assertEquals(FieldValue.Attribute.REPEATED, value.getAttribute());
assertEquals(2, value.getRepeatedValue().size());
for (int i = 0; i < 2; i++) {
FieldValue record = value.getRepeatedValue().get(i);
assertEquals(FieldValue.Attribute.RECORD, record.getAttribute());
FieldValueList recordValue = record.getRecordValue();
assertEquals(
stringValues[i],
FieldValueList.of(recordValue, subSchema).get("stringField").getValue());
}
}
}
}

@Test
public void testStructQuery() throws InterruptedException {
// query into a table
Expand Down