1pub 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
40pub 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
69pub const FLOW_EXTENSIONS_METADATA_KEY: &str = "x-greptime-flow-extensions";
71pub 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 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 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 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 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 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 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 assert_eq!(encoded_1.len(), 1);
637 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 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 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 let mut encoder = FlightEncoder::default();
730 let mut decoder = FlightDecoder::default();
731
732 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 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 #[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 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, }
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}