Skip to main content

arrow_flight/
decode.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::{FlightData, trailers::LazyTrailers};
19use arrow_array::{ArrayRef, RecordBatch};
20use arrow_buffer::Buffer;
21use arrow_data::UnsafeFlag;
22use arrow_schema::{Schema, SchemaRef};
23use bytes::Bytes;
24use futures::{Stream, StreamExt, ready, stream::BoxStream};
25use std::{collections::HashMap, fmt::Debug, pin::Pin, sync::Arc, task::Poll};
26use tonic::metadata::MetadataMap;
27
28use crate::error::{FlightError, Result};
29
30/// Decodes a [Stream] of [`FlightData`] back into
31/// [`RecordBatch`]es. This can be used to decode the response from an
32/// Arrow Flight server
33///
34/// # Note
35/// To access the lower level Flight messages (e.g. to access
36/// [`FlightData::app_metadata`]), you can call [`Self::into_inner`]
37/// and use the [`FlightDataDecoder`] directly.
38///
39/// # Example:
40/// ```no_run
41/// # async fn f() -> Result<(), arrow_flight::error::FlightError>{
42/// # use bytes::Bytes;
43/// // make a do_get request
44/// use arrow_flight::{
45///   error::Result,
46///   decode::FlightRecordBatchStream,
47///   Ticket,
48///   flight_service_client::FlightServiceClient
49/// };
50/// use tonic::transport::Channel;
51/// use futures::stream::{StreamExt, TryStreamExt};
52///
53/// let client: FlightServiceClient<Channel> = // make client..
54/// # unimplemented!();
55///
56/// let request = tonic::Request::new(
57///   Ticket { ticket: Bytes::new() }
58/// );
59///
60/// // Get a stream of FlightData;
61/// let flight_data_stream = client
62///   .do_get(request)
63///   .await?
64///   .into_inner();
65///
66/// // Decode stream of FlightData to RecordBatches
67/// let record_batch_stream = FlightRecordBatchStream::new_from_flight_data(
68///   // convert tonic::Status to FlightError
69///   flight_data_stream.map_err(|e| e.into())
70/// );
71///
72/// // Read back RecordBatches
73/// while let Some(batch) = record_batch_stream.next().await {
74///   match batch {
75///     Ok(batch) => { /* process batch */ },
76///     Err(e) => { /* handle error */ },
77///   };
78/// }
79///
80/// # Ok(())
81/// # }
82/// ```
83#[derive(Debug)]
84pub struct FlightRecordBatchStream {
85    /// Optional grpc header metadata.
86    headers: MetadataMap,
87
88    /// Optional grpc trailer metadata.
89    trailers: Option<LazyTrailers>,
90
91    inner: FlightDataDecoder,
92}
93
94impl FlightRecordBatchStream {
95    /// Create a new [`FlightRecordBatchStream`] from a decoded stream
96    pub fn new(inner: FlightDataDecoder) -> Self {
97        Self {
98            inner,
99            headers: MetadataMap::default(),
100            trailers: None,
101        }
102    }
103
104    /// Create a new [`FlightRecordBatchStream`] from a stream of [`FlightData`]
105    pub fn new_from_flight_data<S>(inner: S) -> Self
106    where
107        S: Stream<Item = Result<FlightData>> + Send + 'static,
108    {
109        Self {
110            inner: FlightDataDecoder::new(inner),
111            headers: MetadataMap::default(),
112            trailers: None,
113        }
114    }
115
116    /// Record response headers.
117    pub fn with_headers(self, headers: MetadataMap) -> Self {
118        Self { headers, ..self }
119    }
120
121    /// Record response trailers.
122    pub fn with_trailers(self, trailers: LazyTrailers) -> Self {
123        Self {
124            trailers: Some(trailers),
125            ..self
126        }
127    }
128
129    /// Headers attached to this stream.
130    pub fn headers(&self) -> &MetadataMap {
131        &self.headers
132    }
133
134    /// Trailers attached to this stream.
135    ///
136    /// Note that this will return `None` until the entire stream is consumed.
137    /// Only after calling `next()` returns `None`, might any available trailers be returned.
138    pub fn trailers(&self) -> Option<MetadataMap> {
139        self.trailers.as_ref()?.get()
140    }
141
142    /// Return schema for the stream, if it has been received
143    pub fn schema(&self) -> Option<&SchemaRef> {
144        self.inner.schema()
145    }
146
147    /// Consume self and return the wrapped [`FlightDataDecoder`]
148    pub fn into_inner(self) -> FlightDataDecoder {
149        self.inner
150    }
151}
152
153impl futures::Stream for FlightRecordBatchStream {
154    type Item = Result<RecordBatch>;
155
156    /// Returns the next [`RecordBatch`] available in this stream, or `None` if
157    /// there are no further results available.
158    fn poll_next(
159        mut self: Pin<&mut Self>,
160        cx: &mut std::task::Context<'_>,
161    ) -> Poll<Option<Result<RecordBatch>>> {
162        loop {
163            let had_schema = self.schema().is_some();
164            let res = ready!(self.inner.poll_next_unpin(cx));
165            match res {
166                // Inner exhausted
167                None => {
168                    return Poll::Ready(None);
169                }
170                Some(Err(e)) => {
171                    return Poll::Ready(Some(Err(e)));
172                }
173                // translate data
174                Some(Ok(data)) => match data.payload {
175                    DecodedPayload::Schema(_) if had_schema => {
176                        return Poll::Ready(Some(Err(FlightError::protocol(
177                            "Unexpectedly saw multiple Schema messages in FlightData stream",
178                        ))));
179                    }
180                    DecodedPayload::Schema(_) => {
181                        // Need next message, poll inner again
182                    }
183                    DecodedPayload::RecordBatch(batch) => {
184                        return Poll::Ready(Some(Ok(batch)));
185                    }
186                    DecodedPayload::None => {
187                        // Need next message
188                    }
189                },
190            }
191        }
192    }
193}
194
195/// Wrapper around a stream of [`FlightData`] that handles the details
196/// of decoding low level Flight messages into [`Schema`] and
197/// [`RecordBatch`]es, including details such as dictionaries.
198///
199/// # Protocol Details
200///
201/// The client handles flight messages as follows:
202///
203/// - **None:** This message has no effect. This is useful to
204///   transmit metadata without any actual payload.
205///
206/// - **Schema:** The schema is (re-)set. Dictionaries are cleared and
207///   the decoded schema is returned.
208///
209/// - **Dictionary Batch:** A new dictionary for a given column is registered. An existing
210///   dictionary for the same column will be overwritten. This
211///   message is NOT visible.
212///
213/// - **Record Batch:** Record batch is created based on the current
214///   schema and dictionaries. This fails if no schema was transmitted
215///   yet.
216///
217/// All other message types (at the time of writing: e.g. tensor and
218/// sparse tensor) lead to an error.
219///
220/// Example usecases
221///
222/// 1. Using this low level stream it is possible to receive a steam
223///    of RecordBatches in FlightData that have different schemas by
224///    handling multiple schema messages separately.
225pub struct FlightDataDecoder {
226    /// Underlying data stream
227    response: BoxStream<'static, Result<FlightData>>,
228    /// Decoding state
229    state: Option<FlightStreamState>,
230    /// Seen the end of the inner stream?
231    done: bool,
232    /// Skip validation of decoded arrays (UTF-8, offset bounds, null counts).
233    skip_validation: UnsafeFlag,
234}
235
236impl Debug for FlightDataDecoder {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        f.debug_struct("FlightDataDecoder")
239            .field("response", &"<stream>")
240            .field("state", &self.state)
241            .field("done", &self.done)
242            .field("skip_validation", &self.skip_validation)
243            .finish()
244    }
245}
246
247impl FlightDataDecoder {
248    /// Create a new wrapper around the stream of [`FlightData`]
249    pub fn new<S>(response: S) -> Self
250    where
251        S: Stream<Item = Result<FlightData>> + Send + 'static,
252    {
253        Self {
254            state: None,
255            response: response.boxed(),
256            done: false,
257            skip_validation: UnsafeFlag::new(),
258        }
259    }
260
261    /// # Safety
262    /// Invalid data may cause undefined behavior. Only use for trusted senders.
263    pub unsafe fn with_skip_validation(mut self) -> Self {
264        unsafe { self.skip_validation.set(true) };
265        self
266    }
267
268    /// Returns the current schema for this stream
269    pub fn schema(&self) -> Option<&SchemaRef> {
270        self.state.as_ref().map(|state| &state.schema)
271    }
272
273    /// Extracts flight data from the next message, updating decoding
274    /// state as necessary.
275    fn extract_message(&mut self, data: FlightData) -> Result<Option<DecodedFlightData>> {
276        use arrow_ipc::MessageHeader;
277        let message = arrow_ipc::root_as_message(&data.data_header[..])
278            .map_err(|e| FlightError::DecodeError(format!("Error decoding root message: {e}")))?;
279
280        match message.header_type() {
281            MessageHeader::NONE => Ok(Some(DecodedFlightData::new_none(data))),
282            MessageHeader::Schema => {
283                let schema = Schema::try_from(&data)
284                    .map_err(|e| FlightError::DecodeError(format!("Error decoding schema: {e}")))?;
285
286                let schema = Arc::new(schema);
287                let dictionaries_by_field = HashMap::new();
288
289                self.state = Some(FlightStreamState {
290                    schema: Arc::clone(&schema),
291                    dictionaries_by_field,
292                });
293                Ok(Some(DecodedFlightData::new_schema(data, schema)))
294            }
295            MessageHeader::DictionaryBatch => {
296                let Some(state) = self.state.as_mut() else {
297                    return Err(FlightError::protocol(
298                        "Received DictionaryBatch prior to Schema",
299                    ));
300                };
301
302                let buffer = Buffer::from(data.data_body);
303                let dictionary_batch = message.header_as_dictionary_batch().ok_or_else(|| {
304                    FlightError::protocol(
305                        "Could not get dictionary batch from DictionaryBatch message",
306                    )
307                })?;
308
309                arrow_ipc::reader::read_dictionary_impl(
310                    &buffer,
311                    dictionary_batch,
312                    &state.schema,
313                    &mut state.dictionaries_by_field,
314                    &message.version(),
315                    false,
316                    self.skip_validation.clone(),
317                )
318                .map_err(|e| {
319                    FlightError::DecodeError(format!("Error decoding ipc dictionary: {e}"))
320                })?;
321
322                // Updated internal state, but no decoded message
323                Ok(None)
324            }
325            MessageHeader::RecordBatch => {
326                let Some(state) = self.state.as_ref() else {
327                    return Err(FlightError::protocol(
328                        "Received RecordBatch prior to Schema",
329                    ));
330                };
331
332                let record_batch = message.header_as_record_batch().ok_or_else(|| {
333                    FlightError::DecodeError(
334                        "Unable to convert flight data header to a record batch".to_string(),
335                    )
336                })?;
337                let buffer = {
338                    // see https://arrow.apache.org/docs/format/Columnar.html#buffer-alignment-and-padding
339                    // reuse the allocation if already aligned, otherwise copy into a fresh aligned buffer.
340                    let ptr_value = data.data_body.as_ptr() as usize;
341                    if ptr_value.is_multiple_of(64) {
342                        &Buffer::from(data.data_body.clone())
343                    } else {
344                        &Buffer::from(data.data_body.as_ref())
345                    }
346                };
347                let batch = arrow_ipc::reader::RecordBatchDecoder::try_new(
348                    buffer,
349                    record_batch,
350                    Arc::clone(&state.schema),
351                    &state.dictionaries_by_field,
352                    &message.version(),
353                )?
354                .with_skip_validation(self.skip_validation.clone())
355                .read_record_batch()
356                .map_err(|e| {
357                    FlightError::DecodeError(format!("Error decoding ipc RecordBatch: {e}"))
358                })?;
359
360                Ok(Some(DecodedFlightData::new_record_batch(data, batch)))
361            }
362            other => {
363                let name = other.variant_name().unwrap_or("UNKNOWN");
364                Err(FlightError::protocol(format!("Unexpected message: {name}")))
365            }
366        }
367    }
368}
369
370impl futures::Stream for FlightDataDecoder {
371    type Item = Result<DecodedFlightData>;
372    /// Returns the result of decoding the next [`FlightData`] message
373    /// from the server, or `None` if there are no further results
374    /// available.
375    fn poll_next(
376        mut self: Pin<&mut Self>,
377        cx: &mut std::task::Context<'_>,
378    ) -> Poll<Option<Self::Item>> {
379        if self.done {
380            return Poll::Ready(None);
381        }
382        loop {
383            let res = ready!(self.response.poll_next_unpin(cx));
384
385            return Poll::Ready(match res {
386                None => {
387                    self.done = true;
388                    None // inner is exhausted
389                }
390                Some(data) => Some(match data {
391                    Err(e) => Err(e),
392                    Ok(data) => match self.extract_message(data) {
393                        Ok(Some(extracted)) => Ok(extracted),
394                        Ok(None) => continue, // Need next input message
395                        Err(e) => Err(e),
396                    },
397                }),
398            });
399        }
400    }
401}
402
403/// tracks the state needed to reconstruct [`RecordBatch`]es from a
404/// streaming flight response.
405#[derive(Debug)]
406struct FlightStreamState {
407    schema: SchemaRef,
408    dictionaries_by_field: HashMap<i64, ArrayRef>,
409}
410
411/// FlightData and the decoded payload (Schema, RecordBatch), if any
412#[derive(Debug)]
413pub struct DecodedFlightData {
414    /// The original FlightData message
415    pub inner: FlightData,
416    /// The decoded payload
417    pub payload: DecodedPayload,
418}
419
420impl DecodedFlightData {
421    /// Create a new DecodedFlightData with no payload
422    pub fn new_none(inner: FlightData) -> Self {
423        Self {
424            inner,
425            payload: DecodedPayload::None,
426        }
427    }
428
429    /// Create a new DecodedFlightData with a [`Schema`] payload
430    pub fn new_schema(inner: FlightData, schema: SchemaRef) -> Self {
431        Self {
432            inner,
433            payload: DecodedPayload::Schema(schema),
434        }
435    }
436
437    /// Create a new [`DecodedFlightData`] with a [`RecordBatch`] payload
438    pub fn new_record_batch(inner: FlightData, batch: RecordBatch) -> Self {
439        Self {
440            inner,
441            payload: DecodedPayload::RecordBatch(batch),
442        }
443    }
444
445    /// Return the metadata field of the inner flight data
446    pub fn app_metadata(&self) -> Bytes {
447        self.inner.app_metadata.clone()
448    }
449}
450
451/// The result of decoding [`FlightData`]
452#[derive(Debug)]
453pub enum DecodedPayload {
454    /// None (no data was sent in the corresponding FlightData)
455    None,
456
457    /// A decoded Schema message
458    Schema(SchemaRef),
459
460    /// A decoded Record batch.
461    RecordBatch(RecordBatch),
462}