Skip to content

Commit 11f26d3

Browse files
authored
feat(bigquery-jdbc): optimize memory footprint for JSON result set streaming (#13660)
b/529436374 - Centralizes row-unpacking logic into `BigQueryFieldValueListWrapper` using a static, stateless utility pattern to reduce object allocations during JSON result set parsing. - Pre-computes complex column flags (`ARRAY`, `STRUCT`, `RANGE`) outside the page-fetching loop in `BigQueryStatement` to avoid redundant per-row inspection. - Preserves raw `FieldValue` references for complex types (`RANGE`, `ARRAY`, `STRUCT`) during row unpacking to prevent improper string coercion.
1 parent a90b8df commit 11f26d3

6 files changed

Lines changed: 302 additions & 21 deletions

File tree

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4923,12 +4923,14 @@ private void populateQueue(
49234923
FieldList resultSchemaFields) {
49244924
LOG.info("Populating queue with %d results...", collectedResults.size());
49254925
try {
4926+
boolean[] isComplexColumn =
4927+
BigQueryFieldValueListWrapper.createComplexColumnFlags(resultSchemaFields);
49264928
for (FieldValueList sortedRow : collectedResults) {
49274929
if (Thread.currentThread().isInterrupted()) {
49284930
LOG.warning("Interrupted during queue population.");
49294931
break;
49304932
}
4931-
queue.put(BigQueryFieldValueListWrapper.of(resultSchemaFields, sortedRow));
4933+
queue.put(BigQueryFieldValueListWrapper.of(resultSchemaFields, sortedRow, isComplexColumn));
49324934
}
49334935
LOG.info("Finished populating queue.");
49344936
} catch (InterruptedException e) {
@@ -4967,7 +4969,7 @@ private void signalEndOfData(
49674969
try {
49684970
LOG.info("Adding end signal to queue.");
49694971
BigQueryFieldValueListWrapper element =
4970-
BigQueryFieldValueListWrapper.of(resultSchemaFields, null, true);
4972+
BigQueryFieldValueListWrapper.ofEndOfStream(resultSchemaFields);
49714973
if (!queue.offer(element)) {
49724974
boolean wasInterrupted = Thread.interrupted();
49734975
try {

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryFieldValueListWrapper.java

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@
1616

1717
package com.google.cloud.bigquery.jdbc;
1818

19+
import static com.google.cloud.bigquery.jdbc.BigQueryBaseArray.isArray;
20+
import static com.google.cloud.bigquery.jdbc.BigQueryBaseStruct.isStruct;
21+
22+
import com.google.cloud.bigquery.Field;
1923
import com.google.cloud.bigquery.FieldList;
2024
import com.google.cloud.bigquery.FieldValue;
25+
import com.google.cloud.bigquery.FieldValue.Attribute;
2126
import com.google.cloud.bigquery.FieldValueList;
27+
import com.google.cloud.bigquery.StandardSQLTypeName;
2228
import java.util.List;
2329

2430
/**
2531
* Package-private, This class acts as a facade layer and wraps the FieldList(schema) and
26-
* FieldValueList
32+
* FieldValueList or lightweight Object[] row buffers.
2733
*/
2834
class BigQueryFieldValueListWrapper {
2935

@@ -37,57 +43,129 @@ class BigQueryFieldValueListWrapper {
3743
// reference as a List<FieldValue> in case of an Array
3844
private final List<FieldValue> arrayFieldValueList;
3945

46+
// Lightweight row values buffer (Object[]) for streaming parsing
47+
private final Object[] rowValues;
48+
4049
// This flag marks the end of the stream for the ResultSet
4150
private boolean isLast = false;
4251
private final Exception exception;
4352

53+
static BigQueryFieldValueListWrapper ofEndOfStream(FieldList fieldList) {
54+
return new BigQueryFieldValueListWrapper(fieldList, null, null, null, true, null);
55+
}
56+
4457
static BigQueryFieldValueListWrapper of(
45-
FieldList fieldList, FieldValueList fieldValueList, boolean... isLast) {
46-
boolean isLastFlag = isLast != null && isLast.length == 1 && isLast[0];
47-
return new BigQueryFieldValueListWrapper(fieldList, fieldValueList, null, isLastFlag, null);
58+
FieldList fieldList, FieldValueList fieldValueList, boolean[] isComplexColumn) {
59+
boolean[] flags =
60+
isComplexColumn != null ? isComplexColumn : createComplexColumnFlags(fieldList);
61+
Object[] rowValues = unpackRow(fieldValueList, flags);
62+
return new BigQueryFieldValueListWrapper(fieldList, null, null, rowValues, false, null);
63+
}
64+
65+
static boolean[] createComplexColumnFlags(FieldList fieldList) {
66+
if (fieldList == null) {
67+
return new boolean[0];
68+
}
69+
int size = fieldList.size();
70+
boolean[] isComplex = new boolean[size];
71+
for (int i = 0; i < size; i++) {
72+
Field field = fieldList.get(i);
73+
isComplex[i] =
74+
isArray(field)
75+
|| isStruct(field)
76+
|| (field.getType() != null
77+
&& field.getType().getStandardType() == StandardSQLTypeName.RANGE);
78+
}
79+
return isComplex;
80+
}
81+
82+
static Object[] unpackRow(FieldValueList fieldValueList, boolean[] isComplexColumn) {
83+
if (fieldValueList == null) {
84+
return null;
85+
}
86+
int size = fieldValueList.size();
87+
Object[] row = new Object[size];
88+
for (int i = 0; i < size; i++) {
89+
FieldValue fv = fieldValueList.get(i);
90+
if (fv == null || fv.isNull()) {
91+
row[i] = null;
92+
} else if ((i < isComplexColumn.length && isComplexColumn[i])
93+
|| fv.getAttribute() != Attribute.PRIMITIVE) {
94+
row[i] = fv;
95+
} else {
96+
row[i] = fv.getStringValue();
97+
}
98+
}
99+
return row;
48100
}
49101

50102
static BigQueryFieldValueListWrapper getNestedFieldValueListWrapper(
51103
FieldList fieldList, List<FieldValue> arrayFieldValueList, boolean... isLast) {
52104
boolean isLastFlag = isLast != null && isLast.length == 1 && isLast[0];
53105
return new BigQueryFieldValueListWrapper(
54-
fieldList, null, arrayFieldValueList, isLastFlag, null);
106+
fieldList, null, arrayFieldValueList, null, isLastFlag, null);
55107
}
56108

57109
static BigQueryFieldValueListWrapper ofError(Exception exception) {
58-
return new BigQueryFieldValueListWrapper(null, null, null, true, exception);
110+
return new BigQueryFieldValueListWrapper(null, null, null, null, true, exception);
59111
}
60112

61113
private BigQueryFieldValueListWrapper(
62114
FieldList fieldList,
63115
FieldValueList fieldValueList,
64116
List<FieldValue> arrayFieldValueList,
117+
Object[] rowValues,
65118
boolean isLast,
66119
Exception exception) {
67120
this.fieldList = fieldList;
68121
this.fieldValueList = fieldValueList;
69122
this.arrayFieldValueList = arrayFieldValueList;
123+
this.rowValues = rowValues;
70124
this.isLast = isLast;
71125
this.exception = exception;
72126
}
73127

74-
public FieldList getFieldList() {
128+
FieldList getFieldList() {
75129
return this.fieldList;
76130
}
77131

78-
public FieldValueList getFieldValueList() {
132+
FieldValueList getFieldValueList() {
79133
return this.fieldValueList;
80134
}
81135

82-
public List<FieldValue> getArrayFieldValueList() {
136+
List<FieldValue> getArrayFieldValueList() {
83137
return this.arrayFieldValueList;
84138
}
85139

86-
public boolean isLast() {
140+
Object[] getRowValues() {
141+
return this.rowValues;
142+
}
143+
144+
FieldValue get(int index) {
145+
if (this.fieldValueList != null) {
146+
return this.fieldValueList.get(index);
147+
}
148+
if (this.arrayFieldValueList != null) {
149+
return this.arrayFieldValueList.get(index);
150+
}
151+
if (this.rowValues != null && index >= 0 && index < this.rowValues.length) {
152+
Object val = this.rowValues[index];
153+
if (val == null) {
154+
return FieldValue.of(Attribute.PRIMITIVE, null);
155+
}
156+
if (val instanceof FieldValue) {
157+
return (FieldValue) val;
158+
}
159+
return FieldValue.of(Attribute.PRIMITIVE, val.toString());
160+
}
161+
return null;
162+
}
163+
164+
boolean isLast() {
87165
return this.isLast;
88166
}
89167

90-
public Exception getException() {
168+
Exception getException() {
91169
return this.exception;
92170
}
93171
}

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSet.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ private FieldValue getObjectInternal(int columnIndex) throws SQLException {
299299
// non nested, return the value
300300
else {
301301
// SQL Index to 0 based index
302-
value = this.cursor.getFieldValueList().get(columnIndex - 1);
302+
value = this.cursor.get(columnIndex - 1);
303303
}
304304
setWasNull(value.getValue());
305305
return value;

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,9 @@ Future<?> parseAndPopulateRpcDataAsync(
12521252
() -> { // producer thread populating the buffer
12531253
try {
12541254
Iterable<FieldValueList> fieldValueLists;
1255+
boolean[] isComplexColumn =
1256+
BigQueryFieldValueListWrapper.createComplexColumnFlags(
1257+
schema != null ? schema.getFields() : null);
12551258
// as we have to process the first page
12561259
boolean hasRows = true;
12571260
while (hasRows) {
@@ -1288,7 +1291,8 @@ Future<?> parseAndPopulateRpcDataAsync(
12881291
}
12891292
Uninterruptibles.putUninterruptibly(
12901293
bigQueryFieldValueListWrapperBlockingQueue,
1291-
BigQueryFieldValueListWrapper.of(schema.getFields(), fieldValueList));
1294+
BigQueryFieldValueListWrapper.of(
1295+
schema.getFields(), fieldValueList, isComplexColumn));
12921296
results += 1;
12931297
}
12941298
LOG.fine(
@@ -1746,6 +1750,6 @@ private void enqueueBufferError(BlockingQueue<BigQueryFieldValueListWrapper> que
17461750
}
17471751

17481752
private void enqueueBufferEndOfStream(BlockingQueue<BigQueryFieldValueListWrapper> queue) {
1749-
Uninterruptibles.putUninterruptibly(queue, BigQueryFieldValueListWrapper.of(null, null, true));
1753+
Uninterruptibles.putUninterruptibly(queue, BigQueryFieldValueListWrapper.ofEndOfStream(null));
17501754
}
17511755
}

0 commit comments

Comments
 (0)