2525import org .apache .arrow .memory .BufferAllocator ;
2626import org .apache .arrow .memory .RootAllocator ;
2727import org .apache .arrow .vector .FieldVector ;
28+ import org .apache .arrow .vector .TimeStampVector ;
2829import org .apache .arrow .vector .VectorLoader ;
2930import org .apache .arrow .vector .VectorSchemaRoot ;
3031import 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