Skip to main content

arrow_flight/
decode.rs

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