Skip to content

Commit 796b77f

Browse files
committed
fix(bigquery): resolve review comments in ArrowDeserializer
1 parent 20cc629 commit 796b77f

1 file changed

Lines changed: 66 additions & 15 deletions

File tree

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

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.arrow.memory.BufferAllocator;
2626
import org.apache.arrow.memory.RootAllocator;
2727
import org.apache.arrow.vector.FieldVector;
28+
import org.apache.arrow.vector.TimeStampVector;
2829
import org.apache.arrow.vector.VectorLoader;
2930
import org.apache.arrow.vector.VectorSchemaRoot;
3031
import org.apache.arrow.vector.complex.ListVector;
@@ -53,6 +54,10 @@ private static com.google.cloud.bigquery.Field arrowFieldToBigQueryField(Field a
5354
com.google.cloud.bigquery.Field.Builder builder;
5455

5556
if (type instanceof ArrowType.List) {
57+
if (arrowField.getChildren().isEmpty()) {
58+
throw new IllegalArgumentException(
59+
"Arrow List field must have at least one child field: " + name);
60+
}
5661
Field innerField = arrowField.getChildren().get(0);
5762
LegacySQLTypeName innerType = arrowTypeToLegacySQLTypeName(innerField.getType());
5863
builder = com.google.cloud.bigquery.Field.newBuilder(name, innerType);
@@ -115,8 +120,19 @@ static List<FieldValueList> deserializeRecordBatch(
115120
throws IOException {
116121
try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE)) {
117122
List<FieldVector> vectors = new ArrayList<>();
118-
for (Field field : arrowSchema.getFields()) {
119-
vectors.add(field.createVector(allocator));
123+
try {
124+
for (Field field : arrowSchema.getFields()) {
125+
vectors.add(field.createVector(allocator));
126+
}
127+
} catch (Throwable t) {
128+
for (int i = vectors.size() - 1; i >= 0; i--) {
129+
try {
130+
vectors.get(i).close();
131+
} catch (Exception e) {
132+
t.addSuppressed(e);
133+
}
134+
}
135+
throw t;
120136
}
121137
try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
122138
VectorLoader loader = new VectorLoader(root);
@@ -138,6 +154,12 @@ static List<FieldValueList> deserializeRecordBatch(
138154

139155
static FieldValueList arrowRootToFieldValueList(
140156
VectorSchemaRoot root, int rowIndex, Schema schema) {
157+
if (root.getFieldVectors().size() != schema.getFields().size()) {
158+
throw new IllegalArgumentException(
159+
String.format(
160+
"Schema mismatch: Arrow vector count (%d) does not match BigQuery schema field count (%d)",
161+
root.getFieldVectors().size(), schema.getFields().size()));
162+
}
141163
List<FieldValue> fieldValues = new ArrayList<>();
142164
for (int colIndex = 0; colIndex < root.getFieldVectors().size(); colIndex++) {
143165
FieldVector vector = root.getVector(colIndex);
@@ -160,10 +182,13 @@ private static FieldValue arrowVectorToFieldValue(
160182
int start = listVector.getElementStartIndex(rowIndex);
161183
int end = listVector.getElementEndIndex(rowIndex);
162184
List<FieldValue> elements = new ArrayList<>(end - start);
185+
com.google.cloud.bigquery.Field.Builder elementBuilder =
186+
com.google.cloud.bigquery.Field.newBuilder(bqField.getName(), bqField.getType());
187+
if (bqField.getType() == LegacySQLTypeName.RECORD && bqField.getSubFields() != null) {
188+
elementBuilder.setType(LegacySQLTypeName.RECORD, bqField.getSubFields());
189+
}
163190
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();
191+
elementBuilder.setMode(com.google.cloud.bigquery.Field.Mode.NULLABLE).build();
167192
for (int k = start; k < end; k++) {
168193
elements.add(arrowVectorToFieldValue(dataVector, k, elementBqField));
169194
}
@@ -174,6 +199,12 @@ private static FieldValue arrowVectorToFieldValue(
174199
// Handle RECORD/STRUCT fields
175200
if (bqField.getType() == LegacySQLTypeName.RECORD) {
176201
StructVector structVector = (StructVector) vector;
202+
if (structVector.size() != bqField.getSubFields().size()) {
203+
throw new IllegalArgumentException(
204+
String.format(
205+
"Schema mismatch for field '%s': Arrow struct size (%d) does not match BigQuery subfields size (%d)",
206+
bqField.getName(), structVector.size(), bqField.getSubFields().size()));
207+
}
177208
List<FieldValue> elements = new ArrayList<>(structVector.size());
178209
for (int colIndex = 0; colIndex < structVector.size(); colIndex++) {
179210
FieldVector childVector = (FieldVector) structVector.getChildByOrdinal(colIndex);
@@ -184,20 +215,40 @@ private static FieldValue arrowVectorToFieldValue(
184215
FieldValue.Attribute.RECORD, FieldValueList.of(elements, bqField.getSubFields()));
185216
}
186217

187-
// Handle primitive types - convert everything to String representations to match BQ standard
188-
Object value = vector.getObject(rowIndex);
218+
// Handle primitive types
189219
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.
220+
if (bqField.getType() == LegacySQLTypeName.TIMESTAMP) {
221+
// Arrow timestamps are long values representing epoch seconds/millis/micros/nanos.
194222
// Standard BigQuery JSON returns timestamps as string of epoch seconds with micro precision
195223
// (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);
224+
TimeStampVector tsVector = (TimeStampVector) vector;
225+
long rawVal = tsVector.get(rowIndex);
226+
ArrowType.Timestamp tsType = (ArrowType.Timestamp) vector.getField().getType();
227+
long micros;
228+
switch (tsType.getUnit()) {
229+
case SECOND:
230+
micros = rawVal * 1_000_000L;
231+
break;
232+
case MILLISECOND:
233+
micros = rawVal * 1_000L;
234+
break;
235+
case MICROSECOND:
236+
micros = rawVal;
237+
break;
238+
case NANOSECOND:
239+
micros = rawVal / 1_000L;
240+
break;
241+
default:
242+
micros = rawVal;
243+
}
244+
stringVal = String.format(Locale.US, "%.6f", micros / 1_000_000.0);
199245
} else {
200-
stringVal = String.valueOf(value);
246+
Object value = vector.getObject(rowIndex);
247+
if (value instanceof byte[]) {
248+
stringVal = BaseEncoding.base64().encode((byte[]) value);
249+
} else {
250+
stringVal = String.valueOf(value);
251+
}
201252
}
202253

203254
return FieldValue.of(FieldValue.Attribute.PRIMITIVE, stringVal);

0 commit comments

Comments
 (0)