Skip to content
This repository was archived by the owner on Aug 30, 2026. It is now read-only.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ implementation 'com.google.cloud:google-cloud-bigquery'
If you are using Gradle without BOM, add this to your dependencies:

```Groovy
implementation 'com.google.cloud:google-cloud-bigquery:2.26.0'
implementation 'com.google.cloud:google-cloud-bigquery:2.26.1'
```

If you are using SBT, add this to your dependencies:

```Scala
libraryDependencies += "com.google.cloud" % "google-cloud-bigquery" % "2.26.0"
libraryDependencies += "com.google.cloud" % "google-cloud-bigquery" % "2.26.1"
```
<!-- {x-version-update-end} -->

Expand Down Expand Up @@ -348,7 +348,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
[kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-bigquery/java11.html
[stability-image]: https://img.shields.io/badge/stability-stable-green
[maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-bigquery.svg
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigquery/2.26.0
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigquery/2.26.1
[authentication]: https://github.com/googleapis/google-cloud-java#authentication
[auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
[predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,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 +526,15 @@ QueryParameterType toTypePb() {
QueryParameterType typePb = new QueryParameterType();
typePb.setType(getType().toString());
if (getArrayType() != null) {
QueryParameterType arrayTypePb = new QueryParameterType();
arrayTypePb.setType(getArrayType().toString());
typePb.setArrayType(arrayTypePb);
List<QueryParameterValue> values = getArrayValues();
if (getArrayType() == StandardSQLTypeName.STRUCT && values != null && values.size() != 0) {
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,163 @@ public void testStructNamedQueryParameters() throws InterruptedException {
}
}

@Test
public void testRepeatedRecordNamedQueryParameters() throws InterruptedException {

Copy link
Copy Markdown
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
Copy Markdown
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 testUnnestRepeatedRecordNamedQueryParameter() throws InterruptedException {
Boolean[] boolValues = new Boolean[] {true, false};
List<QueryParameterValue> tuples = new ArrayList<>();
for (int i = 0; i < 2; i++) {
QueryParameterValue boolValue = QueryParameterValue.bool(boolValues[i]);
Map<String, QueryParameterValue> struct = new HashMap<>();
struct.put("boolField", boolValue);
QueryParameterValue recordValue = QueryParameterValue.struct(struct);
tuples.add(recordValue);
}

QueryParameterValue repeatedRecord =
QueryParameterValue.array(tuples.toArray(), StandardSQLTypeName.STRUCT);
String query =
"SELECT * FROM (SELECT STRUCT("
+ boolValues[0]
+ " AS boolField) AS repeatedRecord) WHERE repeatedRecord IN UNNEST(@repeatedRecordField)";
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.RECORD, value.getAttribute());
FieldValueList recordValue = value.getRecordValue();
assertEquals(
boolValues[0],
FieldValueList.of(recordValue, subSchema).get("boolField").getBooleanValue());
}
}
}

@Test
public void testUnnestRepeatedRecordNamedQueryParameterFromDataset() throws InterruptedException {
List<QueryParameterValue> tuples = new ArrayList<>();
QueryParameterValue statusValue = QueryParameterValue.string("single");
QueryParameterValue addressValue = QueryParameterValue.string("123 this lane");
QueryParameterValue cityValue = QueryParameterValue.string("Torono");
QueryParameterValue stateValue = QueryParameterValue.string("ON");
QueryParameterValue zipValue = QueryParameterValue.string("1h2j34");
QueryParameterValue numberOfYearsValue = QueryParameterValue.string("3");

Map<String, QueryParameterValue> struct = new HashMap<>();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The order of fields inside structs matters when doing comparisons, and HashMap does not maintain insertion order. Because of these reasons, the IN UNNEST(addresses) query doesn't return any results.

I tried replacing HashMap with a LinkedHashMap and using ImmutableMap.of, both options made the IN UNNEST(addresses) query work as intended and return the correct row.

struct.put("statusValue", statusValue);
struct.put("addressValue", addressValue);
struct.put("cityValue", cityValue);
struct.put("stateValue", stateValue);
struct.put("zipValue", zipValue);
struct.put("numberOfYearsValue", numberOfYearsValue);
QueryParameterValue recordValue = QueryParameterValue.struct(struct);
tuples.add(recordValue);

QueryParameterValue repeatedRecord =
QueryParameterValue.array(tuples.toArray(), StandardSQLTypeName.STRUCT);

String unnestRecord = "SELECT TEMP from `neenutestproject.AVRODATASET.NESTEDREPEATEDTABLE`, UNNEST(@repeatedRecord) AS TEMP ";
QueryJobConfiguration queryJobConfiguration = QueryJobConfiguration.newBuilder(unnestRecord)
.setUseLegacySql(false)
.addNamedParameter("repeatedRecord", repeatedRecord)
.build();
TableResult unnestResult = bigquery.query(queryJobConfiguration);
FieldValueList rowValue = unnestResult.getValues().iterator().next();
//Why is this printing the one row 4 times?
unnestResult
.iterateAll()
.forEach(row -> row.forEach(val -> System.out.printf("%s\n", val.toString())));

System.out.println("**************FROM DATASET****************");
// addresses is a REPEATED RECORD field with an array of Structs of the above type.
// @repeatedRecord is array of struct type namedQueryParameter with 1 struct
// the UI does return the correct 1 row output
// this test returns 0 results.
String query = "SELECT * FROM `neenutestproject.AVRODATASET.NESTEDREPEATEDTABLE`, UNNEST(@repeatedRecord) AS TEMP where TEMP IN UNNEST(addresses);";
QueryJobConfiguration queryConfig =
QueryJobConfiguration.newBuilder(query)
.setUseLegacySql(false)
.addNamedParameter("repeatedRecord", repeatedRecord)
.build();
TableResult results = bigquery.query(queryConfig);

System.out.println( Iterables.size(results.getValues()));
results
.iterateAll()
.forEach(row -> row.forEach(val -> System.out.printf("%s", val.toString())));
System.out.println("Query with Array of struct parameter performed successfully.");
}

@Test
public void testEmptyRepeatedRecordNamedQueryParameters() throws InterruptedException {
QueryParameterValue[] tuples = {};

QueryParameterValue repeatedRecord =
QueryParameterValue.array(tuples, StandardSQLTypeName.STRUCT);
String query =
"SELECT * FROM (SELECT STRUCT(false AS boolField) AS repeatedRecord) WHERE repeatedRecord IN UNNEST(@repeatedRecordField)";
QueryJobConfiguration config =
QueryJobConfiguration.newBuilder(query)
.setDefaultDataset(DATASET)
.setUseLegacySql(false)
.addNamedParameter("repeatedRecordField", repeatedRecord)
.build();
try {
bigquery.query(config);
fail("an empty array of struct query parameter shouldn't work with 'IN UNNEST'");
} catch (BigQueryException e) {
// Nothing to do
}
}

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