1616
1717package com .google .cloud .bigquery ;
1818
19+ import com .google .cloud .bigquery .storage .v1 .ReadRowsResponse ;
1920import com .google .common .collect .ImmutableList ;
2021import com .google .common .io .BaseEncoding ;
2122import java .io .IOException ;
23+ import java .nio .channels .Channels ;
2224import java .util .ArrayList ;
25+ import java .util .Iterator ;
2326import java .util .List ;
2427import java .util .Locale ;
2528import org .apache .arrow .memory .BufferAllocator ;
4245 */
4346final 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