Skip to content

Commit e149afc

Browse files
committed
feat(bigquery): add ArrowDeserializer helper utility
1 parent 4f0d80d commit e149afc

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.bigquery;
18+
19+
import com.google.common.collect.ImmutableList;
20+
import com.google.common.io.BaseEncoding;
21+
import java.io.IOException;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import java.util.Locale;
25+
import org.apache.arrow.memory.BufferAllocator;
26+
import org.apache.arrow.memory.RootAllocator;
27+
import org.apache.arrow.vector.FieldVector;
28+
import org.apache.arrow.vector.VectorLoader;
29+
import org.apache.arrow.vector.VectorSchemaRoot;
30+
import org.apache.arrow.vector.complex.ListVector;
31+
import org.apache.arrow.vector.complex.StructVector;
32+
import org.apache.arrow.vector.ipc.ReadChannel;
33+
import org.apache.arrow.vector.ipc.message.MessageSerializer;
34+
import org.apache.arrow.vector.types.pojo.ArrowType;
35+
import org.apache.arrow.vector.types.pojo.Field;
36+
import org.apache.arrow.vector.util.ByteArrayReadableSeekableByteChannel;
37+
38+
final class ArrowDeserializer {
39+
40+
private ArrowDeserializer() {}
41+
42+
static Schema arrowSchemaToBigQuerySchema(org.apache.arrow.vector.types.pojo.Schema arrowSchema) {
43+
List<com.google.cloud.bigquery.Field> fields = new ArrayList<>();
44+
for (Field arrowField : arrowSchema.getFields()) {
45+
fields.add(arrowFieldToBigQueryField(arrowField));
46+
}
47+
return Schema.of(fields);
48+
}
49+
50+
private static com.google.cloud.bigquery.Field arrowFieldToBigQueryField(Field arrowField) {
51+
String name = arrowField.getName();
52+
ArrowType type = arrowField.getType();
53+
com.google.cloud.bigquery.Field.Builder builder;
54+
55+
if (type instanceof ArrowType.List) {
56+
Field innerField = arrowField.getChildren().get(0);
57+
LegacySQLTypeName innerType = arrowTypeToLegacySQLTypeName(innerField.getType());
58+
builder = com.google.cloud.bigquery.Field.newBuilder(name, innerType);
59+
builder.setMode(com.google.cloud.bigquery.Field.Mode.REPEATED);
60+
if (!innerField.getChildren().isEmpty()) {
61+
List<com.google.cloud.bigquery.Field> subFields = new ArrayList<>();
62+
for (Field childField : innerField.getChildren()) {
63+
subFields.add(arrowFieldToBigQueryField(childField));
64+
}
65+
builder.setType(LegacySQLTypeName.RECORD, FieldList.of(subFields));
66+
}
67+
} else {
68+
LegacySQLTypeName bqType = arrowTypeToLegacySQLTypeName(type);
69+
builder = com.google.cloud.bigquery.Field.newBuilder(name, bqType);
70+
if (arrowField.isNullable()) {
71+
builder.setMode(com.google.cloud.bigquery.Field.Mode.NULLABLE);
72+
} else {
73+
builder.setMode(com.google.cloud.bigquery.Field.Mode.REQUIRED);
74+
}
75+
if (!arrowField.getChildren().isEmpty()) {
76+
List<com.google.cloud.bigquery.Field> subFields = new ArrayList<>();
77+
for (Field childField : arrowField.getChildren()) {
78+
subFields.add(arrowFieldToBigQueryField(childField));
79+
}
80+
builder.setType(LegacySQLTypeName.RECORD, FieldList.of(subFields));
81+
}
82+
}
83+
return builder.build();
84+
}
85+
86+
private static LegacySQLTypeName arrowTypeToLegacySQLTypeName(ArrowType type) {
87+
switch (type.getTypeID()) {
88+
case Int:
89+
return LegacySQLTypeName.INTEGER;
90+
case FloatingPoint:
91+
return LegacySQLTypeName.FLOAT;
92+
case Utf8:
93+
return LegacySQLTypeName.STRING;
94+
case Bool:
95+
return LegacySQLTypeName.BOOLEAN;
96+
case Binary:
97+
return LegacySQLTypeName.BYTES;
98+
case Decimal:
99+
return LegacySQLTypeName.NUMERIC;
100+
case Timestamp:
101+
return LegacySQLTypeName.TIMESTAMP;
102+
case Date:
103+
return LegacySQLTypeName.DATE;
104+
case Time:
105+
return LegacySQLTypeName.TIME;
106+
case Struct:
107+
return LegacySQLTypeName.RECORD;
108+
default:
109+
throw new IllegalArgumentException("Unsupported Arrow type: " + type.getTypeID());
110+
}
111+
}
112+
113+
static List<FieldValueList> deserializeRecordBatch(
114+
byte[] recordBatchBytes, Schema schema, org.apache.arrow.vector.types.pojo.Schema arrowSchema)
115+
throws IOException {
116+
try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE)) {
117+
List<FieldVector> vectors = new ArrayList<>();
118+
for (Field field : arrowSchema.getFields()) {
119+
vectors.add(field.createVector(allocator));
120+
}
121+
try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
122+
VectorLoader loader = new VectorLoader(root);
123+
try (org.apache.arrow.vector.ipc.message.ArrowRecordBatch deserializedBatch =
124+
MessageSerializer.deserializeRecordBatch(
125+
new ReadChannel(new ByteArrayReadableSeekableByteChannel(recordBatchBytes)),
126+
allocator)) {
127+
loader.load(deserializedBatch);
128+
int rowCount = root.getRowCount();
129+
List<FieldValueList> rows = new ArrayList<>(rowCount);
130+
for (int i = 0; i < rowCount; i++) {
131+
rows.add(arrowRootToFieldValueList(root, i, schema));
132+
}
133+
return ImmutableList.copyOf(rows);
134+
}
135+
}
136+
}
137+
}
138+
139+
static FieldValueList arrowRootToFieldValueList(
140+
VectorSchemaRoot root, int rowIndex, Schema schema) {
141+
List<FieldValue> fieldValues = new ArrayList<>();
142+
for (int colIndex = 0; colIndex < root.getFieldVectors().size(); colIndex++) {
143+
FieldVector vector = root.getVector(colIndex);
144+
com.google.cloud.bigquery.Field bqField = schema.getFields().get(colIndex);
145+
fieldValues.add(arrowVectorToFieldValue(vector, rowIndex, bqField));
146+
}
147+
return FieldValueList.of(fieldValues, schema.getFields());
148+
}
149+
150+
private static FieldValue arrowVectorToFieldValue(
151+
FieldVector vector, int rowIndex, com.google.cloud.bigquery.Field bqField) {
152+
if (vector.isNull(rowIndex)) {
153+
return FieldValue.of(FieldValue.Attribute.PRIMITIVE, null);
154+
}
155+
156+
// Handle repeated fields
157+
if (bqField.getMode() == com.google.cloud.bigquery.Field.Mode.REPEATED) {
158+
ListVector listVector = (ListVector) vector;
159+
FieldVector dataVector = (FieldVector) listVector.getDataVector();
160+
int start = listVector.getElementStartIndex(rowIndex);
161+
int end = listVector.getElementEndIndex(rowIndex);
162+
List<FieldValue> elements = new ArrayList<>(end - start);
163+
com.google.cloud.bigquery.Field elementBqField =
164+
com.google.cloud.bigquery.Field.newBuilder(bqField.getName(), bqField.getType())
165+
.setMode(com.google.cloud.bigquery.Field.Mode.NULLABLE)
166+
.build();
167+
for (int k = start; k < end; k++) {
168+
elements.add(arrowVectorToFieldValue(dataVector, k, elementBqField));
169+
}
170+
return FieldValue.of(
171+
FieldValue.Attribute.REPEATED, FieldValueList.of(elements, bqField.getSubFields()));
172+
}
173+
174+
// Handle RECORD/STRUCT fields
175+
if (bqField.getType() == LegacySQLTypeName.RECORD) {
176+
StructVector structVector = (StructVector) vector;
177+
List<FieldValue> elements = new ArrayList<>(structVector.size());
178+
for (int colIndex = 0; colIndex < structVector.size(); colIndex++) {
179+
FieldVector childVector = (FieldVector) structVector.getChildByOrdinal(colIndex);
180+
com.google.cloud.bigquery.Field childBqField = bqField.getSubFields().get(colIndex);
181+
elements.add(arrowVectorToFieldValue(childVector, rowIndex, childBqField));
182+
}
183+
return FieldValue.of(
184+
FieldValue.Attribute.RECORD, FieldValueList.of(elements, bqField.getSubFields()));
185+
}
186+
187+
// Handle primitive types - convert everything to String representations to match BQ standard
188+
Object value = vector.getObject(rowIndex);
189+
String stringVal;
190+
if (value instanceof byte[]) {
191+
stringVal = BaseEncoding.base64().encode((byte[]) value);
192+
} else if (bqField.getType() == LegacySQLTypeName.TIMESTAMP) {
193+
// Arrow timestamps are long values representing epoch micro/milli/nano seconds.
194+
// Standard BigQuery JSON returns timestamps as string of epoch seconds with micro precision
195+
// (e.g. "1408452095.220000").
196+
long micros = (long) value;
197+
// Convert to seconds with 6 decimal places of precision
198+
stringVal = String.format(Locale.US, "%.6f", micros / 1000000.0);
199+
} else {
200+
stringVal = String.valueOf(value);
201+
}
202+
203+
return FieldValue.of(FieldValue.Attribute.PRIMITIVE, stringVal);
204+
}
205+
}

0 commit comments

Comments
 (0)