Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,105 +45,13 @@ final class ArrowDeserializer {
private ArrowDeserializer() {}

/**
* Converts an Apache Arrow {@link org.apache.arrow.vector.types.pojo.Schema} to a BigQuery Veneer
* {@link Schema}.
* Converts an Apache Arrow Schema to a BigQuery Veneer {@link Schema}.
*
* @param arrowSchema the Apache Arrow schema to convert
* @return the corresponding BigQuery Veneer Schema
*/
static Schema arrowSchemaToBigQuerySchema(org.apache.arrow.vector.types.pojo.Schema arrowSchema) {
List<Field> fields = new ArrayList<>();
for (org.apache.arrow.vector.types.pojo.Field arrowField : arrowSchema.getFields()) {
fields.add(arrowFieldToBigQueryField(arrowField));
}
return Schema.of(fields);
}

/**
* Recursively converts an Apache Arrow {@link org.apache.arrow.vector.types.pojo.Field} to a
* BigQuery Veneer {@link Field}.
*
* @param arrowField the Arrow field to convert
* @return the corresponding BigQuery Veneer Field
*/
private static Field arrowFieldToBigQueryField(
org.apache.arrow.vector.types.pojo.Field arrowField) {
String name = arrowField.getName();
ArrowType type = arrowField.getType();
Field.Builder builder;

if (type instanceof ArrowType.List) {
if (arrowField.getChildren().isEmpty()) {
throw new IllegalArgumentException(
"Arrow List field must have at least one child field: " + name);
}
org.apache.arrow.vector.types.pojo.Field innerField = arrowField.getChildren().get(0);
LegacySQLTypeName innerType = arrowTypeToLegacySQLTypeName(innerField.getType());
builder = Field.newBuilder(name, innerType);
builder.setMode(Field.Mode.REPEATED);
if (!innerField.getChildren().isEmpty()) {
List<Field> subFields = new ArrayList<>();
for (org.apache.arrow.vector.types.pojo.Field childField : innerField.getChildren()) {
subFields.add(arrowFieldToBigQueryField(childField));
}
builder.setType(LegacySQLTypeName.RECORD, FieldList.of(subFields));
}
} else {
LegacySQLTypeName bqType = arrowTypeToLegacySQLTypeName(type);
builder = Field.newBuilder(name, bqType);
if (arrowField.isNullable()) {
builder.setMode(Field.Mode.NULLABLE);
} else {
builder.setMode(Field.Mode.REQUIRED);
}
if (!arrowField.getChildren().isEmpty()) {
List<Field> subFields = new ArrayList<>();
for (org.apache.arrow.vector.types.pojo.Field childField : innerFieldChildren(arrowField)) {
subFields.add(arrowFieldToBigQueryField(childField));
}
builder.setType(LegacySQLTypeName.RECORD, FieldList.of(subFields));
}
}
return builder.build();
}

private static List<org.apache.arrow.vector.types.pojo.Field> innerFieldChildren(
org.apache.arrow.vector.types.pojo.Field arrowField) {
return arrowField.getChildren();
}

/**
* Maps an Apache Arrow data type {@link ArrowType} to a BigQuery {@link LegacySQLTypeName}.
*
* @param type the Arrow data type to map
* @return the corresponding BigQuery LegacySQLTypeName
* @throws IllegalArgumentException if the Arrow type is unsupported
*/
private static LegacySQLTypeName arrowTypeToLegacySQLTypeName(ArrowType type) {
switch (type.getTypeID()) {
case Int:
return LegacySQLTypeName.INTEGER;
case FloatingPoint:
return LegacySQLTypeName.FLOAT;
case Utf8:
return LegacySQLTypeName.STRING;
case Bool:
return LegacySQLTypeName.BOOLEAN;
case Binary:
return LegacySQLTypeName.BYTES;
case Decimal:
return LegacySQLTypeName.NUMERIC;
case Timestamp:
return LegacySQLTypeName.TIMESTAMP;
case Date:
return LegacySQLTypeName.DATE;
case Time:
return LegacySQLTypeName.TIME;
case Struct:
return LegacySQLTypeName.RECORD;
default:
throw new IllegalArgumentException("Unsupported Arrow type: " + type.getTypeID());
}
static Schema arrowSchemaToBigQuerySchema(Object arrowSchema) {
return ArrowPojoUtils.arrowSchemaToBigQuerySchema(arrowSchema);
}

/**
Expand All @@ -160,13 +68,24 @@ private static LegacySQLTypeName arrowTypeToLegacySQLTypeName(ArrowType type) {
* @throws IOException if deserialization of the Arrow record batch fails
*/
static List<FieldValueList> deserializeRecordBatch(
byte[] recordBatchBytes, Schema schema, org.apache.arrow.vector.types.pojo.Schema arrowSchema)
throws IOException {
byte[] recordBatchBytes, Schema schema, Object arrowSchema) throws IOException {
try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE)) {
List<FieldVector> vectors = new ArrayList<>();
List<FieldVector> vectors = ArrowPojoUtils.createVectors(arrowSchema, allocator);
try {
for (org.apache.arrow.vector.types.pojo.Field field : arrowSchema.getFields()) {
vectors.add(field.createVector(allocator));
try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
VectorLoader loader = new VectorLoader(root);
try (ArrowRecordBatch deserializedBatch =
MessageSerializer.deserializeRecordBatch(
new ReadChannel(new ByteArrayReadableSeekableByteChannel(recordBatchBytes)),
allocator)) {
loader.load(deserializedBatch);
int rowCount = root.getRowCount();
List<FieldValueList> rows = new ArrayList<>(rowCount);
for (int i = 0; i < rowCount; i++) {
rows.add(arrowRootToFieldValueList(root, i, schema));
}
return ImmutableList.copyOf(rows);
}
}
} catch (Throwable t) {
for (int i = vectors.size() - 1; i >= 0; i--) {
Expand All @@ -178,21 +97,6 @@ static List<FieldValueList> deserializeRecordBatch(
}
throw t;
}
try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
VectorLoader loader = new VectorLoader(root);
try (ArrowRecordBatch deserializedBatch =
MessageSerializer.deserializeRecordBatch(
new ReadChannel(new ByteArrayReadableSeekableByteChannel(recordBatchBytes)),
allocator)) {
loader.load(deserializedBatch);
int rowCount = root.getRowCount();
List<FieldValueList> rows = new ArrayList<>(rowCount);
for (int i = 0; i < rowCount; i++) {
rows.add(arrowRootToFieldValueList(root, i, schema));
}
return ImmutableList.copyOf(rows);
}
}
}
}

Expand Down Expand Up @@ -281,9 +185,6 @@ private static FieldValue arrowVectorToFieldValue(
// Handle primitive types
String stringVal;
if (bqField.getType() == LegacySQLTypeName.TIMESTAMP) {
// Arrow timestamps are long values representing epoch seconds/millis/micros/nanos.
// Standard BigQuery JSON returns timestamps as string of epoch seconds with micro precision
// (e.g. "1408452095.220000").
TimeStampVector tsVector = (TimeStampVector) vector;
long rawVal = tsVector.get(rowIndex);
ArrowType.Timestamp tsType = (ArrowType.Timestamp) vector.getField().getType();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.bigquery;

import java.util.ArrayList;
import java.util.List;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.vector.FieldVector;
import org.apache.arrow.vector.types.pojo.ArrowType;

/** Internal helper for Apache Arrow Schema/Field conversions. */
final class ArrowPojoUtils {

private ArrowPojoUtils() {}

static Schema arrowSchemaToBigQuerySchema(Object arrowSchemaObj) {
org.apache.arrow.vector.types.pojo.Schema arrowSchema =
(org.apache.arrow.vector.types.pojo.Schema) arrowSchemaObj;
List<Field> fields = new ArrayList<>();
for (org.apache.arrow.vector.types.pojo.Field arrowField : arrowSchema.getFields()) {
fields.add(arrowFieldToBigQueryField(arrowField));
}
return Schema.of(fields);
}

static Field arrowFieldToBigQueryField(Object arrowFieldObj) {
org.apache.arrow.vector.types.pojo.Field arrowField =
(org.apache.arrow.vector.types.pojo.Field) arrowFieldObj;
String name = arrowField.getName();
ArrowType type = arrowField.getType();
Field.Builder builder;

if (type instanceof ArrowType.List) {
if (arrowField.getChildren().isEmpty()) {
throw new IllegalArgumentException(
"Arrow List field must have at least one child field: " + name);
}
org.apache.arrow.vector.types.pojo.Field innerField = arrowField.getChildren().get(0);
LegacySQLTypeName innerType = arrowTypeToLegacySQLTypeName(innerField.getType());
builder = Field.newBuilder(name, innerType);
builder.setMode(Field.Mode.REPEATED);
if (!innerField.getChildren().isEmpty()) {
List<Field> subFields = new ArrayList<>();
for (org.apache.arrow.vector.types.pojo.Field childField : innerField.getChildren()) {
subFields.add(arrowFieldToBigQueryField(childField));
}
builder.setType(LegacySQLTypeName.RECORD, FieldList.of(subFields));
}
} else {
LegacySQLTypeName bqType = arrowTypeToLegacySQLTypeName(type);
builder = Field.newBuilder(name, bqType);
if (arrowField.isNullable()) {
builder.setMode(Field.Mode.NULLABLE);
} else {
builder.setMode(Field.Mode.REQUIRED);
}
if (!arrowField.getChildren().isEmpty()) {
List<Field> subFields = new ArrayList<>();
for (org.apache.arrow.vector.types.pojo.Field childField : arrowField.getChildren()) {
subFields.add(arrowFieldToBigQueryField(childField));
}
builder.setType(LegacySQLTypeName.RECORD, FieldList.of(subFields));
}
}
return builder.build();
}

static List<FieldVector> createVectors(Object arrowSchemaObj, BufferAllocator allocator) {
org.apache.arrow.vector.types.pojo.Schema arrowSchema =
(org.apache.arrow.vector.types.pojo.Schema) arrowSchemaObj;
List<FieldVector> vectors = new ArrayList<>();
for (org.apache.arrow.vector.types.pojo.Field field : arrowSchema.getFields()) {
vectors.add(field.createVector(allocator));
}
return vectors;
}
Comment on lines +82 to +90

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.

high

If an exception is thrown during field.createVector(allocator) (for example, due to allocator capacity limits or other runtime issues), any vectors already added to the vectors list will not be closed, leading to a memory/resource leak. Wrapping the loop in a try-catch block to close any already allocated vectors in the catch block before rethrowing the exception prevents this leak.

  static List<FieldVector> createVectors(Object arrowSchemaObj, BufferAllocator allocator) {
    org.apache.arrow.vector.types.pojo.Schema arrowSchema =
        (org.apache.arrow.vector.types.pojo.Schema) arrowSchemaObj;
    List<FieldVector> vectors = new ArrayList<>();
    try {
      for (org.apache.arrow.vector.types.pojo.Field field : arrowSchema.getFields()) {
        vectors.add(field.createVector(allocator));
      }
      return vectors;
    } catch (Throwable t) {
      for (int i = vectors.size() - 1; i >= 0; i--) {
        try {
          vectors.get(i).close();
        } catch (Exception e) {
          // Suppress exception during cleanup to preserve original exception
        }
      }
      throw t;
    }
  }
References
  1. When managing a collection of closeable resources (e.g., scopes), ensure they are closed in the reverse order of their creation (LIFO). The implementation must be exception-safe to prevent resource leaks, meaning all opened resources should be closed even if exceptions occur during their creation or closing.

Comment on lines +82 to +90

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.

high

Wrap the vector creation loop in a try-catch block to close any successfully created vectors in reverse order (LIFO) if an exception is thrown during the process. This ensures exception safety and prevents resource leaks.

  static List<FieldVector> createVectors(Object arrowSchemaObj, BufferAllocator allocator) {
    org.apache.arrow.vector.types.pojo.Schema arrowSchema =
        (org.apache.arrow.vector.types.pojo.Schema) arrowSchemaObj;
    List<FieldVector> vectors = new ArrayList<>();
    try {
      for (org.apache.arrow.vector.types.pojo.Field field : arrowSchema.getFields()) {
        vectors.add(field.createVector(allocator));
      }
      return vectors;
    } catch (Throwable t) {
      for (int i = vectors.size() - 1; i >= 0; i--) {
        try {
          vectors.get(i).close();
        } catch (Exception e) {
          // ignore
        }
      }
      throw t;
    }
  }
References
  1. When managing a collection of closeable resources, ensure they are closed in the reverse order of their creation (LIFO). The implementation must be exception-safe to prevent resource leaks, meaning all opened resources should be closed even if exceptions occur during their creation or closing.


private static LegacySQLTypeName arrowTypeToLegacySQLTypeName(ArrowType type) {
switch (type.getTypeID()) {
case Int:
return LegacySQLTypeName.INTEGER;
case FloatingPoint:
return LegacySQLTypeName.FLOAT;
case Utf8:
return LegacySQLTypeName.STRING;
case Bool:
return LegacySQLTypeName.BOOLEAN;
case Binary:
return LegacySQLTypeName.BYTES;
case Decimal:
return LegacySQLTypeName.NUMERIC;
case Timestamp:
return LegacySQLTypeName.TIMESTAMP;
case Date:
return LegacySQLTypeName.DATE;
case Time:
return LegacySQLTypeName.TIME;
case Struct:
return LegacySQLTypeName.RECORD;
default:
throw new IllegalArgumentException("Unsupported Arrow type: " + type.getTypeID());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ com.google.api.services.bigquery.model.ArrowSerializationOptions toPb() {
return ArrowSerializationOptionsConverter.toPb(this);
}

static ArrowSerializationOptions fromPb(
com.google.api.services.bigquery.model.ArrowSerializationOptions optionsPb) {
static ArrowSerializationOptions fromPb(Object optionsPb) {
return ArrowSerializationOptionsConverter.fromPb(optionsPb);
}

Expand Down
Loading
Loading