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().and_then(|trailers| trailers.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 followes:
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 state = if let Some(state) = self.state.as_mut() {
297 state
298 } else {
299 return Err(FlightError::protocol(
300 "Received DictionaryBatch prior to Schema",
301 ));
302 };
303
304 let buffer = Buffer::from(data.data_body);
305 let dictionary_batch = message.header_as_dictionary_batch().ok_or_else(|| {
306 FlightError::protocol(
307 "Could not get dictionary batch from DictionaryBatch message",
308 )
309 })?;
310
311 arrow_ipc::reader::read_dictionary_impl(
312 &buffer,
313 dictionary_batch,
314 &state.schema,
315 &mut state.dictionaries_by_field,
316 &message.version(),
317 false,
318 self.skip_validation.clone(),
319 )
320 .map_err(|e| {
321 FlightError::DecodeError(format!("Error decoding ipc dictionary: {e}"))
322 })?;
323
324 // Updated internal state, but no decoded message
325 Ok(None)
326 }
327 MessageHeader::RecordBatch => {
328 let state = if let Some(state) = self.state.as_ref() {
329 state
330 } else {
331 return Err(FlightError::protocol(
332 "Received RecordBatch prior to Schema",
333 ));
334 };
335
336 let record_batch = message.header_as_record_batch().ok_or_else(|| {
337 FlightError::DecodeError(
338 "Unable to convert flight data header to a record batch".to_string(),
339 )
340 })?;
341 let buffer = {
342 // see https://arrow.apache.org/docs/format/Columnar.html#buffer-alignment-and-padding
343 // reuse the allocation if already aligned, otherwise copy into a fresh aligned buffer.
344 if data.data_body.as_ptr() as usize % 64 == 0 {
345 &Buffer::from(data.data_body.clone())
346 } else {
347 &Buffer::from(data.data_body.as_ref())
348 }
349 };
350 let batch = arrow_ipc::reader::RecordBatchDecoder::try_new(
351 buffer,
352 record_batch,
353 Arc::clone(&state.schema),
354 &state.dictionaries_by_field,
355 &message.version(),
356 )?
357 .with_skip_validation(self.skip_validation.clone())
358 .read_record_batch()
359 .map_err(|e| {
360 FlightError::DecodeError(format!("Error decoding ipc RecordBatch: {e}"))
361 })?;
362
363 Ok(Some(DecodedFlightData::new_record_batch(data, batch)))
364 }
365 other => {
366 let name = other.variant_name().unwrap_or("UNKNOWN");
367 Err(FlightError::protocol(format!("Unexpected message: {name}")))
368 }
369 }
370 }
371}
372
373impl futures::Stream for FlightDataDecoder {
374 type Item = Result<DecodedFlightData>;
375 /// Returns the result of decoding the next [`FlightData`] message
376 /// from the server, or `None` if there are no further results
377 /// available.
378 fn poll_next(
379 mut self: Pin<&mut Self>,
380 cx: &mut std::task::Context<'_>,
381 ) -> Poll<Option<Self::Item>> {
382 if self.done {
383 return Poll::Ready(None);
384 }
385 loop {
386 let res = ready!(self.response.poll_next_unpin(cx));
387
388 return Poll::Ready(match res {
389 None => {
390 self.done = true;
391 None // inner is exhausted
392 }
393 Some(data) => Some(match data {
394 Err(e) => Err(e),
395 Ok(data) => match self.extract_message(data) {
396 Ok(Some(extracted)) => Ok(extracted),
397 Ok(None) => continue, // Need next input message
398 Err(e) => Err(e),
399 },
400 }),
401 });
402 }
403 }
404}
405
406/// tracks the state needed to reconstruct [`RecordBatch`]es from a
407/// streaming flight response.
408#[derive(Debug)]
409struct FlightStreamState {
410 schema: SchemaRef,
411 dictionaries_by_field: HashMap<i64, ArrayRef>,
412}
413
414/// FlightData and the decoded payload (Schema, RecordBatch), if any
415#[derive(Debug)]
416pub struct DecodedFlightData {
417 /// The original FlightData message
418 pub inner: FlightData,
419 /// The decoded payload
420 pub payload: DecodedPayload,
421}
422
423impl DecodedFlightData {
424 /// Create a new DecodedFlightData with no payload
425 pub fn new_none(inner: FlightData) -> Self {
426 Self {
427 inner,
428 payload: DecodedPayload::None,
429 }
430 }
431
432 /// Create a new DecodedFlightData with a [`Schema`] payload
433 pub fn new_schema(inner: FlightData, schema: SchemaRef) -> Self {
434 Self {
435 inner,
436 payload: DecodedPayload::Schema(schema),
437 }
438 }
439
440 /// Create a new [`DecodedFlightData`] with a [`RecordBatch`] payload
441 pub fn new_record_batch(inner: FlightData, batch: RecordBatch) -> Self {
442 Self {
443 inner,
444 payload: DecodedPayload::RecordBatch(batch),
445 }
446 }
447
448 /// Return the metadata field of the inner flight data
449 pub fn app_metadata(&self) -> Bytes {
450 self.inner.app_metadata.clone()
451 }
452}
453
454/// The result of decoding [`FlightData`]
455#[derive(Debug)]
456pub enum DecodedPayload {
457 /// None (no data was sent in the corresponding FlightData)
458 None,
459
460 /// A decoded Schema message
461 Schema(SchemaRef),
462
463 /// A decoded Record batch.
464 RecordBatch(RecordBatch),
465}