Skip to main content

common_grpc/
flight.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15pub mod do_put;
16
17use std::collections::HashMap;
18use std::sync::Arc;
19
20use api::v1::{AffectedRows, FlightMetadata, Metrics};
21use arrow_flight::FlightData;
22use arrow_flight::utils::flight_data_to_arrow_batch;
23use common_base::bytes::Bytes;
24use common_recordbatch::DfRecordBatch;
25use datatypes::arrow;
26use datatypes::arrow::array::ArrayRef;
27use datatypes::arrow::buffer::Buffer;
28use datatypes::arrow::datatypes::{Schema as ArrowSchema, SchemaRef};
29use datatypes::arrow::error::ArrowError;
30use datatypes::arrow::ipc::{MessageHeader, convert, reader, root_as_message, writer};
31use flatbuffers::FlatBufferBuilder;
32use prost::Message;
33use prost::bytes::Bytes as ProstBytes;
34use snafu::{OptionExt, ResultExt};
35use vec1::{Vec1, vec1};
36
37use crate::error;
38use crate::error::{DecodeFlightDataSnafu, InvalidFlightDataSnafu, Result};
39
40/// Encodes a batch into the schema, record-batch header and payload used by bulk inserts.
41///
42/// Uses the default Flight compression and rejects dictionary batches, whose
43/// additional Flight messages cannot be represented by this three-part format.
44pub fn record_batch_to_ipc(
45    record_batch: DfRecordBatch,
46) -> Result<(ProstBytes, ProstBytes, ProstBytes)> {
47    let mut encoder = FlightEncoder::default();
48    let schema = encoder.encode_schema(record_batch.schema().as_ref());
49    let mut iter = encoder
50        .encode(FlightMessage::RecordBatch(record_batch))
51        .into_iter();
52    let flight_data = iter.next().context(InvalidFlightDataSnafu {
53        reason: "Failed to encode empty flight data",
54    })?;
55    if iter.next().is_some() {
56        return error::NotSupportedSnafu {
57            feat: "bulk insert RecordBatch with dictionary arrays",
58        }
59        .fail();
60    }
61
62    Ok((
63        schema.data_header,
64        flight_data.data_header,
65        flight_data.data_body,
66    ))
67}
68
69/// Flight metadata key used to carry flow query extensions as JSON pairs.
70pub const FLOW_EXTENSIONS_METADATA_KEY: &str = "x-greptime-flow-extensions";
71/// Flight metadata key used to carry query snapshot read upper bounds as JSON.
72pub const SNAPSHOT_SEQS_METADATA_KEY: &str = "x-greptime-snapshot-seqs";
73
74#[derive(Debug, Clone)]
75pub enum FlightMessage {
76    Schema(SchemaRef),
77    RecordBatch(DfRecordBatch),
78    AffectedRows {
79        rows: usize,
80        metrics: Option<String>,
81    },
82    Metrics(String),
83}
84
85pub struct FlightEncoder {
86    write_options: writer::IpcWriteOptions,
87    data_gen: writer::IpcDataGenerator,
88    dictionary_tracker: writer::DictionaryTracker,
89}
90
91impl Default for FlightEncoder {
92    fn default() -> Self {
93        let write_options = writer::IpcWriteOptions::default()
94            .try_with_compression(Some(arrow::ipc::CompressionType::LZ4_FRAME))
95            .unwrap();
96
97        Self {
98            write_options,
99            data_gen: writer::IpcDataGenerator::default(),
100            dictionary_tracker: writer::DictionaryTracker::new(false),
101        }
102    }
103}
104
105impl FlightEncoder {
106    /// Creates new [FlightEncoder] with compression disabled.
107    pub fn with_compression_disabled() -> Self {
108        let write_options = writer::IpcWriteOptions::default()
109            .try_with_compression(None)
110            .unwrap();
111
112        Self {
113            write_options,
114            data_gen: writer::IpcDataGenerator::default(),
115            dictionary_tracker: writer::DictionaryTracker::new(false),
116        }
117    }
118
119    /// Encode the Arrow schema to [FlightData].
120    pub fn encode_schema(&mut self, schema: &ArrowSchema) -> FlightData {
121        self.data_gen
122            .schema_to_bytes_with_dictionary_tracker(
123                schema,
124                &mut self.dictionary_tracker,
125                &self.write_options,
126            )
127            .into()
128    }
129
130    /// Encode the [FlightMessage] to a list (at least one element) of [FlightData]s.
131    ///
132    /// Normally only when the [FlightMessage] is an Arrow [RecordBatch] with dictionary arrays
133    /// will the encoder produce more than one [FlightData]s. Other types of [FlightMessage] should
134    /// be encoded to exactly one [FlightData].
135    pub fn encode(&mut self, flight_message: FlightMessage) -> Vec1<FlightData> {
136        match flight_message {
137            FlightMessage::Schema(schema) => vec1![self.encode_schema(schema.as_ref())],
138            FlightMessage::RecordBatch(record_batch) => {
139                let (encoded_dictionaries, encoded_batch) = self
140                    .data_gen
141                    .encode(
142                        &record_batch,
143                        &mut self.dictionary_tracker,
144                        &self.write_options,
145                        &mut Default::default(),
146                    )
147                    .expect("DictionaryTracker configured above to not fail on replacement");
148
149                Vec1::from_vec_push(
150                    encoded_dictionaries.into_iter().map(Into::into).collect(),
151                    encoded_batch.into(),
152                )
153            }
154            FlightMessage::AffectedRows { rows, metrics } => {
155                let metadata = FlightMetadata {
156                    affected_rows: Some(AffectedRows { value: rows as _ }),
157                    metrics: metrics.map(|s| Metrics {
158                        metrics: s.into_bytes(),
159                    }),
160                }
161                .encode_to_vec();
162                vec1![FlightData {
163                    flight_descriptor: None,
164                    data_header: build_none_flight_msg().into(),
165                    app_metadata: metadata.into(),
166                    data_body: ProstBytes::default(),
167                }]
168            }
169            FlightMessage::Metrics(s) => {
170                let metadata = FlightMetadata {
171                    affected_rows: None,
172                    metrics: Some(Metrics {
173                        metrics: s.as_bytes().to_vec(),
174                    }),
175                }
176                .encode_to_vec();
177                vec1![FlightData {
178                    flight_descriptor: None,
179                    data_header: build_none_flight_msg().into(),
180                    app_metadata: metadata.into(),
181                    data_body: ProstBytes::default(),
182                }]
183            }
184        }
185    }
186}
187
188#[derive(Default)]
189pub struct FlightDecoder {
190    schema: Option<SchemaRef>,
191    schema_bytes: Option<bytes::Bytes>,
192    dictionaries_by_id: HashMap<i64, ArrayRef>,
193}
194
195impl FlightDecoder {
196    /// Build a [FlightDecoder] instance from provided schema bytes.
197    pub fn try_from_schema_bytes(schema_bytes: &bytes::Bytes) -> Result<Self> {
198        let arrow_schema = convert::try_schema_from_flatbuffer_bytes(&schema_bytes[..])
199            .context(error::ArrowSnafu)?;
200        Ok(Self {
201            schema: Some(Arc::new(arrow_schema)),
202            schema_bytes: Some(schema_bytes.clone()),
203            dictionaries_by_id: HashMap::new(),
204        })
205    }
206
207    pub fn try_decode_record_batch(
208        &mut self,
209        data_header: &bytes::Bytes,
210        data_body: &bytes::Bytes,
211    ) -> Result<DfRecordBatch> {
212        let schema = self
213            .schema
214            .as_ref()
215            .context(InvalidFlightDataSnafu {
216                reason: "Should have decoded schema first!",
217            })?
218            .clone();
219        let message = root_as_message(&data_header[..])
220            .map_err(|err| {
221                ArrowError::ParseError(format!("Unable to get root as message: {err:?}"))
222            })
223            .context(error::ArrowSnafu)?;
224        let result = message
225            .header_as_record_batch()
226            .ok_or_else(|| {
227                ArrowError::ParseError(
228                    "Unable to convert flight data header to a record batch".to_string(),
229                )
230            })
231            .and_then(|batch| {
232                reader::read_record_batch(
233                    &Buffer::from(data_body.as_ref()),
234                    batch,
235                    schema,
236                    &HashMap::new(),
237                    None,
238                    &message.version(),
239                )
240            })
241            .context(error::ArrowSnafu)?;
242        Ok(result)
243    }
244
245    /// Try to decode the [FlightData] to a [FlightMessage].
246    ///
247    /// If the [FlightData] is of type `DictionaryBatch` (produced while encoding an Arrow
248    /// [RecordBatch] with dictionary arrays), the decoder will not return any [FlightMessage]s.
249    /// Instead, it will update its internal dictionary cache. Other types of [FlightData] will
250    /// be decoded to exactly one [FlightMessage].
251    pub fn try_decode(&mut self, flight_data: &FlightData) -> Result<Option<FlightMessage>> {
252        let message = root_as_message(&flight_data.data_header).map_err(|e| {
253            InvalidFlightDataSnafu {
254                reason: e.to_string(),
255            }
256            .build()
257        })?;
258        match message.header_type() {
259            MessageHeader::NONE => {
260                let metadata = FlightMetadata::decode(flight_data.app_metadata.clone())
261                    .context(DecodeFlightDataSnafu)?;
262                if let Some(AffectedRows { value }) = metadata.affected_rows {
263                    return Ok(Some(FlightMessage::AffectedRows {
264                        rows: value as _,
265                        metrics: metadata
266                            .metrics
267                            .map(|m| String::from_utf8_lossy(&m.metrics).to_string()),
268                    }));
269                }
270                if let Some(Metrics { metrics }) = metadata.metrics {
271                    return Ok(Some(FlightMessage::Metrics(
272                        String::from_utf8_lossy(&metrics).to_string(),
273                    )));
274                }
275                InvalidFlightDataSnafu {
276                    reason: "Expecting FlightMetadata have some meaningful content.",
277                }
278                .fail()
279            }
280            MessageHeader::Schema => {
281                let arrow_schema = Arc::new(ArrowSchema::try_from(flight_data).map_err(|e| {
282                    InvalidFlightDataSnafu {
283                        reason: e.to_string(),
284                    }
285                    .build()
286                })?);
287                self.schema = Some(arrow_schema.clone());
288                self.schema_bytes = Some(flight_data.data_header.clone());
289                Ok(Some(FlightMessage::Schema(arrow_schema)))
290            }
291            MessageHeader::RecordBatch => {
292                let schema = self.schema.clone().context(InvalidFlightDataSnafu {
293                    reason: "Should have decoded schema first!",
294                })?;
295                let arrow_batch = flight_data_to_arrow_batch(
296                    flight_data,
297                    schema.clone(),
298                    &self.dictionaries_by_id,
299                )
300                .map_err(|e| {
301                    InvalidFlightDataSnafu {
302                        reason: e.to_string(),
303                    }
304                    .build()
305                })?;
306                Ok(Some(FlightMessage::RecordBatch(arrow_batch)))
307            }
308            MessageHeader::DictionaryBatch => {
309                let dictionary_batch =
310                    message
311                        .header_as_dictionary_batch()
312                        .context(InvalidFlightDataSnafu {
313                            reason: "could not get dictionary batch from DictionaryBatch message",
314                        })?;
315
316                let schema = self.schema.as_ref().context(InvalidFlightDataSnafu {
317                    reason: "schema message is not present previously",
318                })?;
319
320                reader::read_dictionary(
321                    &flight_data.data_body.clone().into(),
322                    dictionary_batch,
323                    schema,
324                    &mut self.dictionaries_by_id,
325                    &message.version(),
326                )
327                .context(error::ArrowSnafu)?;
328                Ok(None)
329            }
330            other => {
331                let name = other.variant_name().unwrap_or("UNKNOWN");
332                InvalidFlightDataSnafu {
333                    reason: format!("Unsupported FlightData type: {name}"),
334                }
335                .fail()
336            }
337        }
338    }
339
340    pub fn schema(&self) -> Option<&SchemaRef> {
341        self.schema.as_ref()
342    }
343
344    pub fn schema_bytes(&self) -> Option<bytes::Bytes> {
345        self.schema_bytes.clone()
346    }
347}
348
349pub fn flight_messages_to_recordbatches(
350    messages: Vec<FlightMessage>,
351) -> Result<Vec<DfRecordBatch>> {
352    if messages.is_empty() {
353        Ok(vec![])
354    } else {
355        let mut recordbatches = Vec::with_capacity(messages.len() - 1);
356
357        match &messages[0] {
358            FlightMessage::Schema(_schema) => {}
359            _ => {
360                return InvalidFlightDataSnafu {
361                    reason: "First Flight Message must be schema!",
362                }
363                .fail();
364            }
365        };
366
367        for message in messages.into_iter().skip(1) {
368            match message {
369                FlightMessage::RecordBatch(recordbatch) => recordbatches.push(recordbatch),
370                _ => {
371                    return InvalidFlightDataSnafu {
372                        reason: "Expect the following Flight Messages are all Recordbatches!",
373                    }
374                    .fail();
375                }
376            }
377        }
378
379        Ok(recordbatches)
380    }
381}
382
383fn build_none_flight_msg() -> Bytes {
384    let mut builder = FlatBufferBuilder::new();
385
386    let mut message = arrow::ipc::MessageBuilder::new(&mut builder);
387    message.add_version(arrow::ipc::MetadataVersion::V5);
388    message.add_header_type(MessageHeader::NONE);
389    message.add_bodyLength(0);
390
391    let data = message.finish();
392    builder.finish(data, None);
393
394    builder.finished_data().into()
395}
396
397#[cfg(test)]
398mod test {
399    use arrow_flight::utils::batches_to_flight_data;
400    use datatypes::arrow::array::{
401        DictionaryArray, Int32Array, ListArray, StringArray, UInt8Array, UInt32Array,
402    };
403    use datatypes::arrow::buffer::OffsetBuffer;
404    use datatypes::arrow::datatypes::{DataType, Field, Schema};
405
406    use crate::Error;
407    use crate::flight::*;
408
409    #[test]
410    fn test_record_batch_to_ipc_preserves_wire_bytes() {
411        let schema = Arc::new(Schema::new(vec![Field::new("n", DataType::Int32, true)]));
412        for values in [vec![], vec![Some(1), None, Some(3)]] {
413            let batch =
414                DfRecordBatch::try_new(schema.clone(), vec![Arc::new(Int32Array::from(values))])
415                    .unwrap();
416            // Reproduce the previous Prom encoding sequence and compare all bytes.
417            let mut encoder = FlightEncoder::default();
418            let expected_schema = encoder.encode_schema(batch.schema().as_ref());
419            let messages = encoder.encode(FlightMessage::RecordBatch(batch.clone()));
420            assert_eq!(messages.len(), 1);
421            let expected_batch = messages.first();
422            assert_eq!(
423                record_batch_to_ipc(batch).unwrap(),
424                (
425                    expected_schema.data_header,
426                    expected_batch.data_header.clone(),
427                    expected_batch.data_body.clone(),
428                )
429            );
430        }
431    }
432
433    #[test]
434    fn test_record_batch_to_ipc_rejects_dictionary() {
435        let schema = Arc::new(Schema::new(vec![Field::new_dictionary(
436            "tag",
437            DataType::UInt32,
438            DataType::Utf8,
439            true,
440        )]));
441        let dictionary = DictionaryArray::new(
442            UInt32Array::from_value(0, 3),
443            Arc::new(StringArray::from_iter_values(["x"])),
444        );
445        let batch = DfRecordBatch::try_new(schema, vec![Arc::new(dictionary)]).unwrap();
446        assert!(matches!(
447            record_batch_to_ipc(batch),
448            Err(Error::NotSupported { feat })
449                if feat == "bulk insert RecordBatch with dictionary arrays"
450        ));
451    }
452
453    #[test]
454    fn test_try_decode() -> Result<()> {
455        let schema = Arc::new(ArrowSchema::new(vec![Field::new(
456            "n",
457            DataType::Int32,
458            true,
459        )]));
460
461        let batch1 = DfRecordBatch::try_new(
462            schema.clone(),
463            vec![Arc::new(Int32Array::from(vec![Some(1), None, Some(3)])) as _],
464        )
465        .unwrap();
466        let batch2 = DfRecordBatch::try_new(
467            schema.clone(),
468            vec![Arc::new(Int32Array::from(vec![None, Some(5)])) as _],
469        )
470        .unwrap();
471
472        let flight_data =
473            batches_to_flight_data(&schema, vec![batch1.clone(), batch2.clone()]).unwrap();
474        assert_eq!(flight_data.len(), 3);
475        let [d1, d2, d3] = flight_data.as_slice() else {
476            unreachable!()
477        };
478
479        let decoder = &mut FlightDecoder::default();
480        assert!(decoder.schema.is_none());
481
482        let result = decoder.try_decode(d2);
483        assert!(matches!(result, Err(Error::InvalidFlightData { .. })));
484        assert!(
485            result
486                .unwrap_err()
487                .to_string()
488                .contains("Should have decoded schema first!")
489        );
490
491        let message = decoder.try_decode(d1)?.unwrap();
492        assert!(matches!(message, FlightMessage::Schema(_)));
493        let FlightMessage::Schema(decoded_schema) = message else {
494            unreachable!()
495        };
496        assert_eq!(decoded_schema, schema);
497
498        let _ = decoder.schema.as_ref().unwrap();
499
500        let message = decoder.try_decode(d2)?.unwrap();
501        assert!(matches!(message, FlightMessage::RecordBatch(_)));
502        let FlightMessage::RecordBatch(actual_batch) = message else {
503            unreachable!()
504        };
505        assert_eq!(actual_batch, batch1);
506
507        let message = decoder.try_decode(d3)?.unwrap();
508        assert!(matches!(message, FlightMessage::RecordBatch(_)));
509        let FlightMessage::RecordBatch(actual_batch) = message else {
510            unreachable!()
511        };
512        assert_eq!(actual_batch, batch2);
513        Ok(())
514    }
515
516    #[test]
517    fn test_affected_rows_metrics_encode_decode() -> Result<()> {
518        let metrics = r#"{"region_watermarks":[{"region_id":42,"watermark":7}]}"#;
519        let mut encoder = FlightEncoder::default();
520        let encoded = encoder.encode(FlightMessage::AffectedRows {
521            rows: 3,
522            metrics: Some(metrics.to_string()),
523        });
524
525        assert_eq!(encoded.len(), 1);
526
527        let mut decoder = FlightDecoder::default();
528        let decoded = decoder.try_decode(encoded.first())?.unwrap();
529        let FlightMessage::AffectedRows {
530            rows,
531            metrics: decoded_metrics,
532        } = decoded
533        else {
534            unreachable!()
535        };
536        assert_eq!(rows, 3);
537        assert_eq!(decoded_metrics.as_deref(), Some(metrics));
538
539        let encoded = encoder.encode(FlightMessage::AffectedRows {
540            rows: 5,
541            metrics: None,
542        });
543        let decoded = decoder.try_decode(encoded.first())?.unwrap();
544        let FlightMessage::AffectedRows {
545            rows,
546            metrics: decoded_metrics,
547        } = decoded
548        else {
549            unreachable!()
550        };
551        assert_eq!(rows, 5);
552        assert!(decoded_metrics.is_none());
553
554        Ok(())
555    }
556
557    #[test]
558    fn test_flight_messages_to_recordbatches() {
559        let schema = Arc::new(Schema::new(vec![Field::new("m", DataType::Int32, true)]));
560        let batch1 = DfRecordBatch::try_new(
561            schema.clone(),
562            vec![Arc::new(Int32Array::from(vec![Some(2), None, Some(4)])) as _],
563        )
564        .unwrap();
565        let batch2 = DfRecordBatch::try_new(
566            schema.clone(),
567            vec![Arc::new(Int32Array::from(vec![None, Some(6)])) as _],
568        )
569        .unwrap();
570        let recordbatches = vec![batch1.clone(), batch2.clone()];
571
572        let m1 = FlightMessage::Schema(schema);
573        let m2 = FlightMessage::RecordBatch(batch1);
574        let m3 = FlightMessage::RecordBatch(batch2);
575
576        let result = flight_messages_to_recordbatches(vec![m2.clone(), m1.clone(), m3.clone()]);
577        assert!(matches!(result, Err(Error::InvalidFlightData { .. })));
578        assert!(
579            result
580                .unwrap_err()
581                .to_string()
582                .contains("First Flight Message must be schema!")
583        );
584
585        let result = flight_messages_to_recordbatches(vec![m1.clone(), m2.clone(), m1.clone()]);
586        assert!(matches!(result, Err(Error::InvalidFlightData { .. })));
587        assert!(
588            result
589                .unwrap_err()
590                .to_string()
591                .contains("Expect the following Flight Messages are all Recordbatches!")
592        );
593
594        let actual = flight_messages_to_recordbatches(vec![m1, m2, m3]).unwrap();
595        assert_eq!(actual, recordbatches);
596    }
597
598    #[test]
599    fn test_flight_encode_decode_with_dictionary_array() -> Result<()> {
600        let schema = Arc::new(Schema::new(vec![
601            Field::new("i", DataType::UInt8, true),
602            Field::new_dictionary("s", DataType::UInt32, DataType::Utf8, true),
603        ]));
604        let batch1 = DfRecordBatch::try_new(
605            schema.clone(),
606            vec![
607                Arc::new(UInt8Array::from_iter_values(vec![1, 2, 3])) as _,
608                Arc::new(DictionaryArray::new(
609                    UInt32Array::from_value(0, 3),
610                    Arc::new(StringArray::from_iter_values(["x"])),
611                )) as _,
612            ],
613        )
614        .unwrap();
615        let batch2 = DfRecordBatch::try_new(
616            schema.clone(),
617            vec![
618                Arc::new(UInt8Array::from_iter_values(vec![4, 5, 6, 7, 8])) as _,
619                Arc::new(DictionaryArray::new(
620                    UInt32Array::from_iter_values([0, 1, 2, 2, 3]),
621                    Arc::new(StringArray::from_iter_values(["h", "e", "l", "o"])),
622                )) as _,
623            ],
624        )
625        .unwrap();
626
627        let message_1 = FlightMessage::Schema(schema.clone());
628        let message_2 = FlightMessage::RecordBatch(batch1);
629        let message_3 = FlightMessage::RecordBatch(batch2);
630
631        let mut encoder = FlightEncoder::default();
632        let encoded_1 = encoder.encode(message_1);
633        let encoded_2 = encoder.encode(message_2);
634        let encoded_3 = encoder.encode(message_3);
635        // message 1 is Arrow Schema, should be encoded to one FlightData:
636        assert_eq!(encoded_1.len(), 1);
637        // message 2 and 3 are Arrow RecordBatch with dictionary arrays, should be encoded to
638        // multiple FlightData:
639        assert_eq!(encoded_2.len(), 2);
640        assert_eq!(encoded_3.len(), 2);
641
642        let mut decoder = FlightDecoder::default();
643        let decoded_1 = decoder.try_decode(encoded_1.first())?;
644        let Some(FlightMessage::Schema(actual_schema)) = decoded_1 else {
645            unreachable!()
646        };
647        assert_eq!(actual_schema, schema);
648        let decoded_2 = decoder.try_decode(&encoded_2[0])?;
649        // expected to be a dictionary batch message, decoder should return none:
650        assert!(decoded_2.is_none());
651        let Some(FlightMessage::RecordBatch(decoded_2)) = decoder.try_decode(&encoded_2[1])? else {
652            unreachable!()
653        };
654        let decoded_3 = decoder.try_decode(&encoded_3[0])?;
655        // expected to be a dictionary batch message, decoder should return none:
656        assert!(decoded_3.is_none());
657        let Some(FlightMessage::RecordBatch(decoded_3)) = decoder.try_decode(&encoded_3[1])? else {
658            unreachable!()
659        };
660        let actual = arrow::util::pretty::pretty_format_batches(&[decoded_2, decoded_3])
661            .unwrap()
662            .to_string();
663        let expected = r"
664+---+---+
665| i | s |
666+---+---+
667| 1 | x |
668| 2 | x |
669| 3 | x |
670| 4 | h |
671| 5 | e |
672| 6 | l |
673| 7 | l |
674| 8 | o |
675+---+---+";
676        assert_eq!(actual, expected.trim());
677        Ok(())
678    }
679
680    #[test]
681    fn test_encode_schema_with_nested_dictionary_array() -> Result<()> {
682        let item = Arc::new(Field::new_dictionary(
683            "item",
684            DataType::UInt32,
685            DataType::Utf8,
686            true,
687        ));
688        let schema = Arc::new(Schema::new(vec![Field::new(
689            "tags",
690            DataType::List(item.clone()),
691            true,
692        )]));
693        let values = DictionaryArray::new(
694            UInt32Array::from_iter_values([0, 1, 0]),
695            Arc::new(StringArray::from_iter_values(["host-a", "host-b"])),
696        );
697        let list = ListArray::new(
698            item,
699            OffsetBuffer::from_lengths([2, 1]),
700            Arc::new(values),
701            None,
702        );
703        let batch = DfRecordBatch::try_new(schema.clone(), vec![Arc::new(list)]).unwrap();
704
705        let mut encoder = FlightEncoder::default();
706        let encoded_schema = encoder.encode_schema(schema.as_ref());
707        let encoded_batch = encoder.encode(FlightMessage::RecordBatch(batch.clone()));
708
709        let mut decoder = FlightDecoder::default();
710        assert!(matches!(
711            decoder.try_decode(&encoded_schema)?,
712            Some(FlightMessage::Schema(actual)) if actual == schema
713        ));
714        for data in encoded_batch.iter().take(encoded_batch.len() - 1) {
715            assert!(decoder.try_decode(data)?.is_none());
716        }
717        assert!(matches!(
718            decoder.try_decode(encoded_batch.last())?,
719            Some(FlightMessage::RecordBatch(actual)) if actual == batch
720        ));
721        Ok(())
722    }
723
724    #[test]
725    fn test_affected_rows_roundtrip_through_flight_codec() {
726        // Verify the full FlightEncoder → FlightDecoder pipeline handles
727        // the new FlightMessage::AffectedRows variant with optional inline
728        // metrics without breaking the wire protocol.
729        let mut encoder = FlightEncoder::default();
730        let mut decoder = FlightDecoder::default();
731
732        // Without metrics — same wire format as old `AffectedRows(7)`.
733        let encoded = encoder.encode(FlightMessage::AffectedRows {
734            rows: 7,
735            metrics: None,
736        });
737        let decoded = decoder.try_decode(encoded.first()).unwrap().unwrap();
738        assert!(matches!(
739            decoded,
740            FlightMessage::AffectedRows {
741                rows: 7,
742                metrics: None,
743            }
744        ));
745
746        // With metrics — new capability, row count preserved.
747        let json = r#"{"region_watermarks":[{"region_id":1,"watermark":99}]}"#;
748        let encoded = encoder.encode(FlightMessage::AffectedRows {
749            rows: 42,
750            metrics: Some(json.to_string()),
751        });
752        let decoded = decoder.try_decode(encoded.first()).unwrap().unwrap();
753        assert!(matches!(
754            decoded,
755            FlightMessage::AffectedRows {
756                rows: 42,
757                metrics: Some(_),
758            }
759        ));
760    }
761
762    /// Simulates the wire output of the **old** `FlightMessage::AffectedRows(usize)`
763    /// variant and verifies that the **new** `FlightDecoder` handles it.
764    #[test]
765    fn test_old_affected_rows_format_decoded_by_new_code() {
766        use arrow_flight::FlightData;
767        use prost::bytes::Bytes as ProstBytes;
768
769        // The old encoder produced FlightData whose app_metadata is
770        // FlightMetadata { affected_rows, metrics: None }. The new
771        // `AffectedRows { rows, metrics: Option<String> }` variant with
772        // `metrics: None` produces the exact same wire bytes.
773        let old_wire_bytes = FlightData {
774            flight_descriptor: None,
775            data_header: build_none_flight_msg().into(),
776            app_metadata: FlightMetadata {
777                affected_rows: Some(AffectedRows { value: 99 }),
778                metrics: None, // old format: no metrics field
779            }
780            .encode_to_vec()
781            .into(),
782            data_body: ProstBytes::default(),
783        };
784
785        let mut decoder = FlightDecoder::default();
786        let decoded = decoder.try_decode(&old_wire_bytes).unwrap().unwrap();
787        assert!(matches!(
788            decoded,
789            FlightMessage::AffectedRows {
790                rows: 99,
791                metrics: None,
792            }
793        ));
794    }
795}