Skip to content

Commit 9acfc1f

Browse files
committed
feat(bigquery): sync finalized ArrowDeserializer, ArrowPojoUtils, and tests
1 parent a2486de commit 9acfc1f

4 files changed

Lines changed: 506 additions & 140 deletions

File tree

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/ArrowDeserializer.java

Lines changed: 142 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616

1717
package com.google.cloud.bigquery;
1818

19+
import com.google.cloud.bigquery.storage.v1.ReadRowsResponse;
1920
import com.google.common.collect.ImmutableList;
2021
import com.google.common.io.BaseEncoding;
2122
import java.io.IOException;
23+
import java.nio.channels.Channels;
2224
import java.util.ArrayList;
25+
import java.util.Iterator;
2326
import java.util.List;
2427
import java.util.Locale;
2528
import org.apache.arrow.memory.BufferAllocator;
@@ -42,108 +45,150 @@
4245
*/
4346
final class ArrowDeserializer {
4447

48+
private static class AllocatorHolder {
49+
private static final BufferAllocator ALLOCATOR = new RootAllocator(Long.MAX_VALUE);
50+
}
51+
52+
private static VectorSchemaRoot createVectorSchemaRoot(
53+
org.apache.arrow.vector.types.pojo.Schema arrowSchema, BufferAllocator allocator) {
54+
List<FieldVector> vectors = ArrowPojoUtils.createVectors(arrowSchema, allocator);
55+
try {
56+
return new VectorSchemaRoot(vectors);
57+
} catch (Throwable t) {
58+
for (int i = vectors.size() - 1; i >= 0; i--) {
59+
try {
60+
vectors.get(i).close();
61+
} catch (Exception e) {
62+
t.addSuppressed(e);
63+
}
64+
}
65+
throw t;
66+
}
67+
}
68+
4569
private ArrowDeserializer() {}
4670

4771
/**
48-
* Converts an Apache Arrow {@link org.apache.arrow.vector.types.pojo.Schema} to a BigQuery Veneer
49-
* {@link Schema}.
72+
* Deserializes a raw binary Arrow schema payload into an Apache Arrow Schema object.
5073
*
51-
* @param arrowSchema the Apache Arrow schema to convert
52-
* @return the corresponding BigQuery Veneer Schema
74+
* @param schemaBytes the raw binary Arrow schema payload
75+
* @return the deserialized Apache Arrow Schema object
76+
* @throws IOException if deserialization of the Arrow schema fails
77+
*/
78+
static Object deserializeSchema(byte[] schemaBytes) throws IOException {
79+
return MessageSerializer.deserializeSchema(
80+
new ReadChannel(new ByteArrayReadableSeekableByteChannel(schemaBytes)));
81+
}
82+
83+
/**
84+
* Serializes an Apache Arrow Schema object to its JSON string representation.
85+
*
86+
* @param arrowSchema the Apache Arrow schema object
87+
* @return the JSON string representation, or null if arrowSchema is null
5388
*/
54-
static Schema arrowSchemaToBigQuerySchema(org.apache.arrow.vector.types.pojo.Schema arrowSchema) {
55-
List<Field> fields = new ArrayList<>();
56-
for (org.apache.arrow.vector.types.pojo.Field arrowField : arrowSchema.getFields()) {
57-
fields.add(arrowFieldToBigQueryField(arrowField));
89+
static String arrowSchemaToJson(Object arrowSchema) {
90+
if (arrowSchema == null) {
91+
return null;
92+
}
93+
return ((org.apache.arrow.vector.types.pojo.Schema) arrowSchema).toJson();
94+
}
95+
96+
static Object jsonToArrowSchema(String json) {
97+
if (json == null) {
98+
return null;
99+
}
100+
try {
101+
return org.apache.arrow.vector.types.pojo.Schema.fromJSON(json);
102+
} catch (IOException e) {
103+
throw new IllegalArgumentException("Invalid Arrow schema JSON", e);
58104
}
59-
return Schema.of(fields);
60105
}
61106

62107
/**
63-
* Recursively converts an Apache Arrow {@link org.apache.arrow.vector.types.pojo.Field} to a
64-
* BigQuery Veneer {@link Field}.
108+
* Reads and decodes a batch of Arrow rows from the provided stream iterator into the row batch.
65109
*
66-
* @param arrowField the Arrow field to convert
67-
* @return the corresponding BigQuery Veneer Field
110+
* @param iterator the stream iterator providing ReadRowsResponse messages
111+
* @param arrowSchemaPojo the Arrow schema pojo (or null if restoring from json)
112+
* @param arrowSchemaJson the Arrow schema JSON representation
113+
* @param schema the BigQuery target Schema
114+
* @param rowBatch the destination list for decoded rows
115+
* @param pageSize the maximum number of rows to decode in this batch
116+
* @param totalRowsReturned the running count of rows returned so far
117+
* @param maxResults the maximum total rows allowed across all pages
118+
* @return true if more rows are available in the stream and maxResults has not been reached
119+
* @throws IOException if deserialization fails
68120
*/
69-
private static Field arrowFieldToBigQueryField(
70-
org.apache.arrow.vector.types.pojo.Field arrowField) {
71-
String name = arrowField.getName();
72-
ArrowType type = arrowField.getType();
73-
Field.Builder builder;
121+
static boolean loadArrowRows(
122+
Iterator<ReadRowsResponse> iterator,
123+
Object arrowSchemaPojo,
124+
String arrowSchemaJson,
125+
Schema schema,
126+
List<FieldValueList> rowBatch,
127+
long pageSize,
128+
long totalRowsReturned,
129+
long maxResults)
130+
throws IOException {
131+
org.apache.arrow.vector.types.pojo.Schema arrowSchema =
132+
arrowSchemaPojo instanceof org.apache.arrow.vector.types.pojo.Schema
133+
? (org.apache.arrow.vector.types.pojo.Schema) arrowSchemaPojo
134+
: (arrowSchemaJson != null
135+
? org.apache.arrow.vector.types.pojo.Schema.fromJSON(arrowSchemaJson)
136+
: null);
74137

75-
if (type instanceof ArrowType.List) {
76-
if (arrowField.getChildren().isEmpty()) {
77-
throw new IllegalArgumentException(
78-
"Arrow List field must have at least one child field: " + name);
79-
}
80-
org.apache.arrow.vector.types.pojo.Field innerField = arrowField.getChildren().get(0);
81-
LegacySQLTypeName innerType = arrowTypeToLegacySQLTypeName(innerField.getType());
82-
builder = Field.newBuilder(name, innerType);
83-
builder.setMode(Field.Mode.REPEATED);
84-
if (!innerField.getChildren().isEmpty()) {
85-
List<Field> subFields = new ArrayList<>();
86-
for (org.apache.arrow.vector.types.pojo.Field childField : innerField.getChildren()) {
87-
subFields.add(arrowFieldToBigQueryField(childField));
138+
if (arrowSchema == null) {
139+
return false;
140+
}
141+
142+
try (BufferAllocator childAllocator =
143+
AllocatorHolder.ALLOCATOR.newChildAllocator("loadArrowRows", 0, Long.MAX_VALUE);
144+
VectorSchemaRoot closedRoot = createVectorSchemaRoot(arrowSchema, childAllocator)) {
145+
VectorLoader loader = new VectorLoader(closedRoot);
146+
boolean hasMore = false;
147+
while (rowBatch.size() < pageSize
148+
&& iterator.hasNext()
149+
&& (totalRowsReturned + rowBatch.size() < maxResults)) {
150+
ReadRowsResponse response = iterator.next();
151+
if (response.hasArrowRecordBatch()) {
152+
com.google.cloud.bigquery.storage.v1.ArrowRecordBatch batch =
153+
response.getArrowRecordBatch();
154+
try (ReadChannel readChannel =
155+
new ReadChannel(
156+
Channels.newChannel(batch.getSerializedRecordBatch().newInput()));
157+
ArrowRecordBatch deserializedBatch =
158+
MessageSerializer.deserializeRecordBatch(readChannel, childAllocator)) {
159+
loader.load(deserializedBatch);
160+
int batchRowCount = closedRoot.getRowCount();
161+
int i = 0;
162+
for (; i < batchRowCount; i++) {
163+
if (rowBatch.size() >= pageSize
164+
|| totalRowsReturned + rowBatch.size() >= maxResults) {
165+
break;
166+
}
167+
rowBatch.add(arrowRootToFieldValueList(closedRoot, i, schema));
168+
}
169+
if (i < batchRowCount && (totalRowsReturned + rowBatch.size() < maxResults)) {
170+
hasMore = true;
171+
}
172+
closedRoot.clear();
173+
}
88174
}
89-
builder.setType(LegacySQLTypeName.RECORD, FieldList.of(subFields));
90-
}
91-
} else {
92-
LegacySQLTypeName bqType = arrowTypeToLegacySQLTypeName(type);
93-
builder = Field.newBuilder(name, bqType);
94-
if (arrowField.isNullable()) {
95-
builder.setMode(Field.Mode.NULLABLE);
96-
} else {
97-
builder.setMode(Field.Mode.REQUIRED);
98175
}
99-
if (!arrowField.getChildren().isEmpty()) {
100-
List<Field> subFields = new ArrayList<>();
101-
for (org.apache.arrow.vector.types.pojo.Field childField : innerFieldChildren(arrowField)) {
102-
subFields.add(arrowFieldToBigQueryField(childField));
103-
}
104-
builder.setType(LegacySQLTypeName.RECORD, FieldList.of(subFields));
176+
if (!hasMore) {
177+
hasMore = iterator.hasNext() && (totalRowsReturned + rowBatch.size() < maxResults);
105178
}
179+
return hasMore;
106180
}
107-
return builder.build();
108-
}
109-
110-
private static List<org.apache.arrow.vector.types.pojo.Field> innerFieldChildren(
111-
org.apache.arrow.vector.types.pojo.Field arrowField) {
112-
return arrowField.getChildren();
113181
}
114182

115183
/**
116-
* Maps an Apache Arrow data type {@link ArrowType} to a BigQuery {@link LegacySQLTypeName}.
184+
* Converts an Apache Arrow Schema to a BigQuery Veneer {@link Schema}.
117185
*
118-
* @param type the Arrow data type to map
119-
* @return the corresponding BigQuery LegacySQLTypeName
120-
* @throws IllegalArgumentException if the Arrow type is unsupported
186+
* @param arrowSchema the Apache Arrow schema to convert
187+
* @return the corresponding BigQuery Veneer Schema
121188
*/
122-
private static LegacySQLTypeName arrowTypeToLegacySQLTypeName(ArrowType type) {
123-
switch (type.getTypeID()) {
124-
case Int:
125-
return LegacySQLTypeName.INTEGER;
126-
case FloatingPoint:
127-
return LegacySQLTypeName.FLOAT;
128-
case Utf8:
129-
return LegacySQLTypeName.STRING;
130-
case Bool:
131-
return LegacySQLTypeName.BOOLEAN;
132-
case Binary:
133-
return LegacySQLTypeName.BYTES;
134-
case Decimal:
135-
return LegacySQLTypeName.NUMERIC;
136-
case Timestamp:
137-
return LegacySQLTypeName.TIMESTAMP;
138-
case Date:
139-
return LegacySQLTypeName.DATE;
140-
case Time:
141-
return LegacySQLTypeName.TIME;
142-
case Struct:
143-
return LegacySQLTypeName.RECORD;
144-
default:
145-
throw new IllegalArgumentException("Unsupported Arrow type: " + type.getTypeID());
146-
}
189+
static Schema arrowSchemaToBigQuerySchema(Object arrowSchema) {
190+
return ArrowPojoUtils.arrowSchemaToBigQuerySchema(
191+
(org.apache.arrow.vector.types.pojo.Schema) arrowSchema);
147192
}
148193

149194
/**
@@ -162,37 +207,23 @@ private static LegacySQLTypeName arrowTypeToLegacySQLTypeName(ArrowType type) {
162207
static List<FieldValueList> deserializeRecordBatch(
163208
byte[] recordBatchBytes, Schema schema, org.apache.arrow.vector.types.pojo.Schema arrowSchema)
164209
throws IOException {
165-
try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE)) {
166-
List<FieldVector> vectors = new ArrayList<>();
167-
try {
168-
for (org.apache.arrow.vector.types.pojo.Field field : arrowSchema.getFields()) {
169-
vectors.add(field.createVector(allocator));
170-
}
171-
} catch (Throwable t) {
172-
for (int i = vectors.size() - 1; i >= 0; i--) {
173-
try {
174-
vectors.get(i).close();
175-
} catch (Exception e) {
176-
t.addSuppressed(e);
177-
}
178-
}
179-
throw t;
180-
}
181-
try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
182-
VectorLoader loader = new VectorLoader(root);
183-
try (ArrowRecordBatch deserializedBatch =
184-
MessageSerializer.deserializeRecordBatch(
185-
new ReadChannel(new ByteArrayReadableSeekableByteChannel(recordBatchBytes)),
186-
allocator)) {
187-
loader.load(deserializedBatch);
188-
int rowCount = root.getRowCount();
189-
List<FieldValueList> rows = new ArrayList<>(rowCount);
190-
for (int i = 0; i < rowCount; i++) {
191-
rows.add(arrowRootToFieldValueList(root, i, schema));
192-
}
193-
return ImmutableList.copyOf(rows);
194-
}
210+
try (BufferAllocator childAllocator =
211+
AllocatorHolder.ALLOCATOR.newChildAllocator(
212+
"deserializeRecordBatch", 0, Long.MAX_VALUE);
213+
VectorSchemaRoot closedRoot = createVectorSchemaRoot(arrowSchema, childAllocator);
214+
ByteArrayReadableSeekableByteChannel byteChannel =
215+
new ByteArrayReadableSeekableByteChannel(recordBatchBytes);
216+
ReadChannel readChannel = new ReadChannel(byteChannel);
217+
ArrowRecordBatch deserializedBatch =
218+
MessageSerializer.deserializeRecordBatch(readChannel, childAllocator)) {
219+
VectorLoader loader = new VectorLoader(closedRoot);
220+
loader.load(deserializedBatch);
221+
int rowCount = closedRoot.getRowCount();
222+
List<FieldValueList> rows = new ArrayList<>(rowCount);
223+
for (int i = 0; i < rowCount; i++) {
224+
rows.add(arrowRootToFieldValueList(closedRoot, i, schema));
195225
}
226+
return ImmutableList.copyOf(rows);
196227
}
197228
}
198229

@@ -281,9 +312,6 @@ private static FieldValue arrowVectorToFieldValue(
281312
// Handle primitive types
282313
String stringVal;
283314
if (bqField.getType() == LegacySQLTypeName.TIMESTAMP) {
284-
// Arrow timestamps are long values representing epoch seconds/millis/micros/nanos.
285-
// Standard BigQuery JSON returns timestamps as string of epoch seconds with micro precision
286-
// (e.g. "1408452095.220000").
287315
TimeStampVector tsVector = (TimeStampVector) vector;
288316
long rawVal = tsVector.get(rowIndex);
289317
ArrowType.Timestamp tsType = (ArrowType.Timestamp) vector.getField().getType();

0 commit comments

Comments
 (0)