Skip to main content

client/
database.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
15use std::collections::HashMap;
16use std::pin::Pin;
17use std::str::FromStr;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::sync::{Arc, RwLock};
20use std::task::{Context, Poll};
21use std::time::Duration;
22
23use api::v1::auth_header::AuthScheme;
24#[cfg(feature = "testing")]
25use api::v1::ddl_request::Expr as DdlExpr;
26use api::v1::greptime_database_client::GreptimeDatabaseClient;
27use api::v1::greptime_request::Request;
28use api::v1::query_request::Query;
29#[cfg(feature = "testing")]
30use api::v1::{AlterTableExpr, CreateTableExpr, DdlRequest};
31use api::v1::{
32    AuthHeader, Basic, GreptimeRequest, InsertRequests, QueryRequest, RequestHeader,
33    RowInsertRequests,
34};
35use arc_swap::ArcSwapOption;
36use arrow_flight::{FlightData, Ticket};
37use async_stream::stream;
38use base64::Engine;
39use base64::prelude::BASE64_STANDARD;
40use common_catalog::build_db_string;
41use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
42use common_error::ext::{BoxedError, ErrorExt};
43use common_grpc::flight::do_put::DoPutResponse;
44use common_grpc::flight::{
45    FLOW_EXTENSIONS_METADATA_KEY, FlightDecoder, FlightMessage, SNAPSHOT_SEQS_METADATA_KEY,
46};
47use common_query::Output;
48use common_recordbatch::adapter::RecordBatchMetrics;
49use common_recordbatch::error::ExternalSnafu;
50use common_recordbatch::{OrderOption, RecordBatch, RecordBatchStream, RecordBatchStreamWrapper};
51use common_telemetry::tracing::Span;
52use common_telemetry::tracing_context::W3cTrace;
53use common_telemetry::{error, warn};
54use futures::future;
55use futures_util::{Stream, StreamExt, TryStreamExt};
56use prost::Message;
57use snafu::{IntoError, ResultExt};
58use tonic::metadata::{AsciiMetadataKey, AsciiMetadataValue, MetadataMap, MetadataValue};
59use tonic::transport::Channel;
60
61use crate::error::{
62    ConvertFlightDataSnafu, Error, FlightGetSnafu, FlightStreamSnafu, IllegalFlightMessagesSnafu,
63    InvalidTonicMetadataValueSnafu,
64};
65use crate::flight::{FlightMessageReader, decode_flight_data};
66use crate::{Client, Result, error, from_grpc_response};
67
68type FlightDataStream = Pin<Box<dyn Stream<Item = FlightData> + Send>>;
69
70type DoPutResponseStream = Pin<Box<dyn Stream<Item = Result<DoPutResponse>>>>;
71
72const HINTS_METADATA_KEY: &str = "x-greptime-hints";
73
74/// Terminal metrics associated with a query output.
75///
76/// For streaming outputs, metrics are only final after the stream is fully
77/// drained and [`Self::is_ready`] returns `true`.
78#[derive(Debug, Clone, Default)]
79pub struct OutputMetrics {
80    inner: Arc<OutputMetricsInner>,
81}
82
83#[derive(Debug, Default)]
84struct OutputMetricsInner {
85    metrics: RwLock<Option<RecordBatchMetrics>>,
86    ready: AtomicBool,
87}
88
89impl OutputMetrics {
90    fn new() -> Self {
91        Self::default()
92    }
93
94    /// Replaces the current terminal metrics snapshot.
95    pub fn update(&self, metrics: Option<RecordBatchMetrics>) {
96        *self.inner.metrics.write().unwrap() = metrics;
97    }
98
99    /// Marks the terminal metrics as final for this output.
100    pub fn mark_ready(&self) {
101        let _ = self
102            .inner
103            .ready
104            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire);
105    }
106
107    /// Returns whether terminal metrics are final.
108    ///
109    /// Streaming outputs become ready only after the stream reaches EOF.
110    pub fn is_ready(&self) -> bool {
111        self.inner.ready.load(Ordering::Acquire)
112    }
113
114    /// Returns the latest terminal metrics snapshot, if any.
115    pub fn get(&self) -> Option<RecordBatchMetrics> {
116        self.inner.metrics.read().unwrap().clone()
117    }
118
119    /// Returns proved per-region watermarks.
120    ///
121    /// Entries whose watermark is `None` are intentionally omitted because they
122    /// represent participating regions whose terminal sequence bound was not
123    /// provable.
124    pub fn region_watermark_map(&self) -> Option<std::collections::HashMap<u64, u64>> {
125        Some(
126            self.get()?
127                .region_watermarks
128                .into_iter()
129                .filter_map(|entry| entry.watermark.map(|seq| (entry.region_id, seq)))
130                .collect::<std::collections::HashMap<_, _>>(),
131        )
132    }
133
134    /// Returns all regions that participated in terminal metric collection,
135    /// including entries whose watermark is `None`.
136    pub fn participating_regions(&self) -> Option<std::collections::BTreeSet<u64>> {
137        Some(
138            self.get()?
139                .region_watermarks
140                .into_iter()
141                .map(|entry| entry.region_id)
142                .collect::<std::collections::BTreeSet<_>>(),
143        )
144    }
145}
146
147/// Query output together with a handle for its terminal metrics.
148///
149/// The contained [`OutputMetrics`] lets callers read stream terminal metrics
150/// after consuming `output`. For non-stream outputs, metrics are ready
151/// immediately.
152#[derive(Debug)]
153pub struct OutputWithMetrics {
154    pub output: Output,
155    pub metrics: OutputMetrics,
156}
157
158impl OutputWithMetrics {
159    /// Wraps an output with a terminal metrics handle.
160    ///
161    /// Stream outputs update the handle as the stream is consumed. Non-stream
162    /// outputs are marked ready immediately.
163    pub fn from_output(output: Output) -> Self {
164        let terminal_metrics = OutputMetrics::new();
165        let output = attach_terminal_metrics(output, &terminal_metrics);
166        Self {
167            output,
168            metrics: terminal_metrics,
169        }
170    }
171
172    /// Returns proved per-region watermarks from the terminal metrics.
173    pub fn region_watermark_map(&self) -> Option<std::collections::HashMap<u64, u64>> {
174        self.metrics.region_watermark_map()
175    }
176
177    /// Returns all regions participating in terminal metric collection.
178    pub fn participating_regions(&self) -> Option<std::collections::BTreeSet<u64>> {
179        self.metrics.participating_regions()
180    }
181
182    /// Drops the terminal metrics handle and returns the original output.
183    pub fn into_output(self) -> Output {
184        self.output
185    }
186}
187
188fn parse_terminal_metrics(metrics_json: &str) -> Result<RecordBatchMetrics> {
189    serde_json::from_str(metrics_json).map_err(|e| {
190        IllegalFlightMessagesSnafu {
191            reason: format!("Invalid terminal metrics message: {e}"),
192        }
193        .build()
194    })
195}
196
197struct StreamWithMetrics {
198    stream: common_recordbatch::SendableRecordBatchStream,
199    metrics: OutputMetrics,
200}
201
202impl StreamWithMetrics {
203    fn new(stream: common_recordbatch::SendableRecordBatchStream, metrics: OutputMetrics) -> Self {
204        Self { stream, metrics }
205    }
206
207    fn sync_terminal_metrics(&self) {
208        self.metrics.update(self.stream.metrics());
209    }
210}
211
212impl RecordBatchStream for StreamWithMetrics {
213    fn name(&self) -> &str {
214        self.stream.name()
215    }
216
217    fn schema(&self) -> datatypes::schema::SchemaRef {
218        self.stream.schema()
219    }
220
221    fn output_ordering(&self) -> Option<&[OrderOption]> {
222        self.stream.output_ordering()
223    }
224
225    fn metrics(&self) -> Option<RecordBatchMetrics> {
226        self.sync_terminal_metrics();
227        self.metrics.get()
228    }
229}
230
231impl Stream for StreamWithMetrics {
232    type Item = common_recordbatch::error::Result<RecordBatch>;
233
234    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
235        let polled = Pin::new(&mut self.stream).poll_next(cx);
236        if let Poll::Ready(None) = &polled {
237            self.sync_terminal_metrics();
238            self.metrics.mark_ready();
239        }
240        polled
241    }
242
243    fn size_hint(&self) -> (usize, Option<usize>) {
244        self.stream.size_hint()
245    }
246}
247
248fn attach_terminal_metrics(output: Output, terminal_metrics: &OutputMetrics) -> Output {
249    let Output { data, meta } = output;
250    let data = match data {
251        common_query::OutputData::Stream(stream) => {
252            terminal_metrics.update(stream.metrics());
253            common_query::OutputData::Stream(Box::pin(StreamWithMetrics::new(
254                stream,
255                terminal_metrics.clone(),
256            )))
257        }
258        other => {
259            terminal_metrics.mark_ready();
260            other
261        }
262    };
263    Output::new(data, meta)
264}
265
266async fn output_from_flight_message_stream<S>(
267    remote_addr: String,
268    flight_message_stream: S,
269) -> Result<OutputWithMetrics>
270where
271    S: Stream<Item = Result<FlightMessage>> + Send + Unpin + 'static,
272{
273    let mut reader = FlightMessageReader::new(remote_addr, flight_message_stream);
274    let first_flight_message = reader
275        .read_first()
276        .await
277        .map_err(|error| flight_stream_error(reader.remote_addr(), error))?;
278
279    match first_flight_message {
280        FlightMessage::AffectedRows { rows, metrics } => {
281            let terminal_metrics = OutputMetrics::new();
282            if let Some(metrics) = metrics {
283                terminal_metrics.update(Some(parse_terminal_metrics(&metrics)?));
284            }
285            let next_message = reader
286                .read_next()
287                .await
288                .map_err(|error| flight_stream_error(reader.remote_addr(), error))?;
289            match next_message {
290                None => terminal_metrics.mark_ready(),
291                Some(FlightMessage::Metrics(s)) if terminal_metrics.get().is_none() => {
292                    terminal_metrics.update(Some(parse_terminal_metrics(&s)?));
293                    terminal_metrics.mark_ready();
294                }
295                Some(FlightMessage::Metrics(_)) => {
296                    return IllegalFlightMessagesSnafu {
297                        reason: "'AffectedRows' Flight metadata already carries Metrics and cannot be followed by another Metrics message",
298                    }
299                    .fail();
300                }
301                Some(other) => {
302                    return IllegalFlightMessagesSnafu {
303                        reason: format!(
304                            "'AffectedRows' Flight message can only be followed by a Metrics message, got {other:?}"
305                        ),
306                    }
307                    .fail();
308                }
309            }
310            Ok(OutputWithMetrics {
311                output: Output::new_with_affected_rows(rows),
312                metrics: terminal_metrics,
313            })
314        }
315        FlightMessage::RecordBatch(_) | FlightMessage::Metrics(_) => IllegalFlightMessagesSnafu {
316            reason: "The first flight message cannot be a RecordBatch or Metrics message",
317        }
318        .fail(),
319        FlightMessage::Schema(schema) => {
320            let metrics = Arc::new(ArcSwapOption::from(None));
321            let metrics_ref = metrics.clone();
322            let schema = Arc::new(
323                datatypes::schema::Schema::try_from(schema).context(error::ConvertSchemaSnafu)?,
324            );
325            let schema_cloned = schema.clone();
326            let stream = Box::pin(stream!({
327                loop {
328                    let flight_message = match reader.read_next().await {
329                        Ok(Some(message)) => message,
330                        Ok(None) => break,
331                        Err(error) => {
332                            yield Err(BoxedError::new(flight_stream_error(
333                                reader.remote_addr(),
334                                error,
335                            )))
336                            .context(ExternalSnafu);
337                            break;
338                        }
339                    };
340                    match flight_message {
341                        FlightMessage::RecordBatch(arrow_batch) => {
342                            yield Ok(RecordBatch::from_df_record_batch(
343                                schema_cloned.clone(),
344                                arrow_batch,
345                            ))
346                        }
347                        FlightMessage::Metrics(s) => {
348                            match parse_terminal_metrics(&s) {
349                                Ok(m) => {
350                                    metrics_ref.swap(Some(Arc::new(m)));
351                                }
352                                Err(e) => {
353                                    yield Err(BoxedError::new(e)).context(ExternalSnafu);
354                                }
355                            };
356                        }
357                        FlightMessage::AffectedRows { .. } | FlightMessage::Schema(_) => {
358                            yield IllegalFlightMessagesSnafu {
359                                reason: format!(
360                                    "A Schema message must be succeeded exclusively by a set of RecordBatch messages, flight_message: {:?}",
361                                    flight_message
362                                )
363                            }
364                            .fail()
365                            .map_err(BoxedError::new)
366                            .context(ExternalSnafu);
367                            break;
368                        }
369                    }
370                }
371            }));
372            let record_batch_stream = RecordBatchStreamWrapper {
373                schema,
374                stream,
375                output_ordering: None,
376                metrics,
377                span: Span::current(),
378            };
379            Ok(OutputWithMetrics::from_output(Output::new_with_stream(
380                Box::pin(record_batch_stream),
381            )))
382        }
383    }
384}
385
386fn flight_stream_error(addr: &str, error: Error) -> Error {
387    let tonic_code = error.tonic_code().unwrap_or(tonic::Code::Unknown);
388    let message = error.to_string();
389    if error.status_code().should_log_error() {
390        error!(
391            error; "Failed to receive Flight data, addr: {}, code: {}",
392            addr,
393            tonic_code
394        );
395    }
396
397    FlightStreamSnafu {
398        addr: addr.to_string(),
399        tonic_code,
400        message,
401    }
402    .into_error(BoxedError::new(error))
403}
404
405#[derive(Clone, Debug, Default)]
406pub struct Database {
407    // The "catalog" and "schema" to be used in processing the requests at the server side.
408    // They are the "hint" or "context", just like how the "database" in "USE" statement is treated in MySQL.
409    // They will be carried in the request header.
410    catalog: String,
411    schema: String,
412    // The dbname follows naming rule as out mysql, postgres and http
413    // protocol. The server treat dbname in priority of catalog/schema.
414    dbname: String,
415    // The time zone indicates the time zone where the user is located.
416    // Some queries need to be aware of the user's time zone to perform some specific actions.
417    timezone: String,
418
419    client: Client,
420    ctx: FlightContext,
421}
422
423#[derive(Default)]
424struct FlightRequestOptions {
425    hints: Option<String>,
426    flow_extensions: Option<String>,
427    snapshot_seqs: Option<String>,
428    timeout: Option<Duration>,
429}
430
431impl FlightRequestOptions {
432    fn apply_to<T>(self, request: &mut tonic::Request<T>) -> Result<()> {
433        let metadata = request.metadata_mut();
434        if let Some(hints) = self.hints {
435            Database::put_metadata_value(metadata, HINTS_METADATA_KEY, hints)?;
436        }
437        if let Some(flow_extensions) = self.flow_extensions {
438            Database::put_metadata_value(metadata, FLOW_EXTENSIONS_METADATA_KEY, flow_extensions)?;
439        }
440        if let Some(snapshot_seqs) = self.snapshot_seqs {
441            Database::put_metadata_value(metadata, SNAPSHOT_SEQS_METADATA_KEY, snapshot_seqs)?;
442        }
443        if let Some(timeout) = self.timeout {
444            request.set_timeout(timeout);
445        }
446        Ok(())
447    }
448}
449
450/// A single Flight DoGet request to a [`Database`].
451///
452/// The builder carries request-scoped metadata and options. It does not modify
453/// the underlying [`Database`], so its configuration cannot affect later RPCs.
454pub struct DatabaseFlightRequest<'a> {
455    database: &'a Database,
456    options: FlightRequestOptions,
457}
458
459pub struct DatabaseClient {
460    pub addr: String,
461    pub inner: GreptimeDatabaseClient<Channel>,
462}
463
464impl DatabaseClient {
465    /// Returns a closure that logs the error when the request fails.
466    pub fn inspect_err<'a>(&'a self, context: &'a str) -> impl Fn(&tonic::Status) + 'a {
467        let addr = &self.addr;
468        move |status| {
469            error!("Failed to {context} request, peer: {addr}, status: {status:?}");
470        }
471    }
472}
473
474fn make_database_client(client: &Client) -> Result<DatabaseClient> {
475    let (addr, channel) = client.find_channel()?;
476    Ok(DatabaseClient {
477        addr,
478        inner: GreptimeDatabaseClient::new(channel)
479            .max_decoding_message_size(client.max_grpc_recv_message_size())
480            .max_encoding_message_size(client.max_grpc_send_message_size()),
481    })
482}
483
484impl Database {
485    /// Create database service client using catalog and schema
486    pub fn new(catalog: impl Into<String>, schema: impl Into<String>, client: Client) -> Self {
487        Self {
488            catalog: catalog.into(),
489            schema: schema.into(),
490            dbname: String::default(),
491            timezone: String::default(),
492            client,
493            ctx: FlightContext::default(),
494        }
495    }
496
497    /// Create database service client using dbname.
498    ///
499    /// This API is designed for external usage. `dbname` is:
500    ///
501    /// - the name of database when using GreptimeDB standalone or cluster
502    /// - the name provided by GreptimeCloud or other multi-tenant GreptimeDB
503    ///   environment
504    pub fn new_with_dbname(dbname: impl Into<String>, client: Client) -> Self {
505        Self {
506            catalog: String::default(),
507            schema: String::default(),
508            timezone: String::default(),
509            dbname: dbname.into(),
510            client,
511            ctx: FlightContext::default(),
512        }
513    }
514
515    /// Set the catalog for the database client.
516    pub fn set_catalog(&mut self, catalog: impl Into<String>) {
517        self.catalog = catalog.into();
518    }
519
520    fn catalog_or_default(&self) -> &str {
521        if self.catalog.is_empty() {
522            DEFAULT_CATALOG_NAME
523        } else {
524            &self.catalog
525        }
526    }
527
528    /// Set the schema for the database client.
529    pub fn set_schema(&mut self, schema: impl Into<String>) {
530        self.schema = schema.into();
531    }
532
533    fn schema_or_default(&self) -> &str {
534        if self.schema.is_empty() {
535            DEFAULT_SCHEMA_NAME
536        } else {
537            &self.schema
538        }
539    }
540
541    /// Set the timezone for the database client.
542    pub fn set_timezone(&mut self, timezone: impl Into<String>) {
543        self.timezone = timezone.into();
544    }
545
546    /// Set the auth scheme for the database client.
547    pub fn set_auth(&mut self, auth: AuthScheme) {
548        self.ctx.auth_header = Some(AuthHeader {
549            auth_scheme: Some(auth),
550        });
551    }
552
553    /// Creates a builder for a single Flight DoGet request.
554    pub fn flight_request(&self) -> DatabaseFlightRequest<'_> {
555        DatabaseFlightRequest {
556            database: self,
557            options: FlightRequestOptions::default(),
558        }
559    }
560
561    /// Make an InsertRequests request to the database.
562    pub async fn insert(&self, requests: InsertRequests) -> Result<u32> {
563        self.handle(Request::Inserts(requests)).await
564    }
565
566    /// Make an InsertRequests request to the database with hints.
567    pub async fn insert_with_hints(
568        &self,
569        requests: InsertRequests,
570        hints: &[(&str, &str)],
571    ) -> Result<u32> {
572        let mut client = make_database_client(&self.client)?;
573        let request = self.to_rpc_request(Request::Inserts(requests));
574
575        let mut request = tonic::Request::new(request);
576        let metadata = request.metadata_mut();
577        Self::put_hints(metadata, hints)?;
578
579        let response = client
580            .inner
581            .handle(request)
582            .await
583            .inspect_err(client.inspect_err("insert_with_hints"))?
584            .into_inner();
585        from_grpc_response(response)
586    }
587
588    /// Make a RowInsertRequests request to the database.
589    pub async fn row_inserts(&self, requests: RowInsertRequests) -> Result<u32> {
590        self.handle(Request::RowInserts(requests)).await
591    }
592
593    /// Make a RowInsertRequests request to the database with hints.
594    pub async fn row_inserts_with_hints(
595        &self,
596        requests: RowInsertRequests,
597        hints: &[(&str, &str)],
598    ) -> Result<u32> {
599        let mut client = make_database_client(&self.client)?;
600        let request = self.to_rpc_request(Request::RowInserts(requests));
601
602        let mut request = tonic::Request::new(request);
603        let metadata = request.metadata_mut();
604        Self::put_hints(metadata, hints)?;
605
606        let response = client
607            .inner
608            .handle(request)
609            .await
610            .inspect_err(client.inspect_err("row_inserts_with_hints"))?
611            .into_inner();
612        from_grpc_response(response)
613    }
614
615    fn put_hints(metadata: &mut MetadataMap, hints: &[(&str, &str)]) -> Result<()> {
616        let Some(value) = Self::encode_hints(hints) else {
617            return Ok(());
618        };
619
620        Self::put_metadata_value(metadata, HINTS_METADATA_KEY, value)
621    }
622
623    fn encode_hints(hints: &[(&str, &str)]) -> Option<String> {
624        hints
625            .iter()
626            .map(|(k, v)| format!("{}={}", k, v))
627            .reduce(|a, b| format!("{},{}", a, b))
628    }
629
630    fn encode_flow_extensions(flow_extensions: &[(&str, &str)]) -> Option<String> {
631        (!flow_extensions.is_empty()).then(|| {
632            serde_json::to_string(&flow_extensions.to_vec())
633                .expect("flow extension pairs should serialize")
634        })
635    }
636
637    fn encode_snapshot_seqs(snapshot_seqs: &HashMap<u64, u64>) -> Option<String> {
638        (!snapshot_seqs.is_empty()).then(|| {
639            serde_json::to_string(snapshot_seqs).expect("snapshot sequence map should serialize")
640        })
641    }
642
643    fn put_metadata_value(
644        metadata: &mut MetadataMap,
645        key: &'static str,
646        value: String,
647    ) -> Result<()> {
648        let key = AsciiMetadataKey::from_static(key);
649        let value = AsciiMetadataValue::from_str(&value).context(InvalidTonicMetadataValueSnafu)?;
650        metadata.insert(key, value);
651        Ok(())
652    }
653
654    /// Make a request to the database.
655    pub async fn handle(&self, request: Request) -> Result<u32> {
656        let mut client = make_database_client(&self.client)?;
657        let request = self.to_rpc_request(request);
658        let response = client
659            .inner
660            .handle(request)
661            .await
662            .inspect_err(client.inspect_err("handle"))?
663            .into_inner();
664        from_grpc_response(response)
665    }
666
667    /// Retry if connection fails, max_retries is the max number of retries, so the total wait time
668    /// is `max_retries * GRPC_CONN_TIMEOUT`
669    pub async fn handle_with_retry(
670        &self,
671        request: Request,
672        max_retries: u32,
673        hints: &[(&str, &str)],
674    ) -> Result<u32> {
675        let mut client = make_database_client(&self.client)?;
676        let mut retries = 0;
677
678        let request = self.to_rpc_request(request);
679
680        loop {
681            let mut tonic_request = tonic::Request::new(request.clone());
682            let metadata = tonic_request.metadata_mut();
683            Self::put_hints(metadata, hints)?;
684            let raw_response = client
685                .inner
686                .handle(tonic_request)
687                .await
688                .inspect_err(client.inspect_err("handle"));
689            match (raw_response, retries < max_retries) {
690                (Ok(resp), _) => return from_grpc_response(resp.into_inner()),
691                (Err(err), true) => {
692                    // determine if the error is retryable
693                    if is_grpc_retryable(&err) {
694                        // retry
695                        retries += 1;
696                        warn!("Retrying {} times with error = {:?}", retries, err);
697                        continue;
698                    } else {
699                        error!(
700                            err; "Failed to send request to grpc handle, retries = {}, not retryable error, aborting",
701                            retries
702                        );
703                        return Err(err.into());
704                    }
705                }
706                (Err(err), false) => {
707                    error!(
708                        err; "Failed to send request to grpc handle after {} retries",
709                        retries,
710                    );
711                    return Err(err.into());
712                }
713            }
714        }
715    }
716
717    #[inline]
718    fn to_rpc_request(&self, request: Request) -> GreptimeRequest {
719        GreptimeRequest {
720            header: Some(RequestHeader {
721                catalog: self.catalog.clone(),
722                schema: self.schema.clone(),
723                authorization: self.ctx.auth_header.clone(),
724                dbname: self.dbname.clone(),
725                timezone: self.timezone.clone(),
726                // TODO(Taylor-lagrange): add client grpc tracing
727                tracing_context: W3cTrace::new(),
728            }),
729            request: Some(request),
730        }
731    }
732
733    /// Executes a SQL query without any hints.
734    pub async fn sql<S>(&self, sql: S) -> Result<Output>
735    where
736        S: AsRef<str>,
737    {
738        self.flight_request().sql(sql).await
739    }
740
741    /// Executes a SQL query with optional hints for query optimization.
742    pub async fn sql_with_hint<S>(&self, sql: S, hints: &[(&str, &str)]) -> Result<Output>
743    where
744        S: AsRef<str>,
745    {
746        self.flight_request().with_hints(hints).sql(sql).await
747    }
748
749    /// Executes a SQL query and returns the output with terminal metrics.
750    ///
751    /// For stream outputs, callers must consume the stream before reading final
752    /// terminal metrics from [`OutputWithMetrics::metrics`].
753    pub async fn sql_with_terminal_metrics<S>(
754        &self,
755        sql: S,
756        hints: &[(&str, &str)],
757    ) -> Result<OutputWithMetrics>
758    where
759        S: AsRef<str>,
760    {
761        self.flight_request()
762            .with_hints(hints)
763            .sql_with_terminal_metrics(sql)
764            .await
765    }
766
767    /// Executes a logical plan directly without SQL parsing.
768    pub async fn logical_plan(&self, logical_plan: Vec<u8>) -> Result<Output> {
769        self.flight_request().logical_plan(logical_plan).await
770    }
771
772    /// Creates a new table using the provided table expression.
773    #[cfg(feature = "testing")]
774    pub async fn create(&self, expr: CreateTableExpr) -> Result<Output> {
775        self.flight_request().create(expr).await
776    }
777
778    /// Alters an existing table using the provided alter expression.
779    #[cfg(feature = "testing")]
780    pub async fn alter(&self, expr: AlterTableExpr) -> Result<Output> {
781        self.flight_request().alter(expr).await
782    }
783
784    async fn do_get(
785        &self,
786        request: Request,
787        options: FlightRequestOptions,
788    ) -> Result<OutputWithMetrics> {
789        let request = self.to_rpc_request(request);
790        let request = Ticket {
791            ticket: request.encode_to_vec().into(),
792        };
793
794        let mut request = tonic::Request::new(request);
795        options.apply_to(&mut request)?;
796
797        let mut client = self.client.make_flight_client(false, false)?;
798        let remote_addr = client.addr().to_string();
799
800        let response = client.mut_inner().do_get(request).await.or_else(|e| {
801            let tonic_code = e.code();
802            let e: Error = e.into();
803            error!(
804                "Failed to do Flight get, addr: {}, code: {}, source: {:?}",
805                client.addr(),
806                tonic_code,
807                e
808            );
809            Err(BoxedError::new(e)).with_context(|_| FlightGetSnafu {
810                addr: remote_addr.clone(),
811                tonic_code,
812            })
813        })?;
814
815        let flight_data_stream = response.into_inner();
816        let mut decoder = FlightDecoder::default();
817
818        let flight_message_stream = flight_data_stream.filter_map(move |flight_data| {
819            future::ready(decode_flight_data(&mut decoder, flight_data))
820        });
821
822        output_from_flight_message_stream(remote_addr, flight_message_stream).await
823    }
824
825    /// Ingest a stream of [RecordBatch]es that belong to a table, using Arrow Flight's "`DoPut`"
826    /// method. The return value is also a stream, produces [DoPutResponse]s.
827    pub async fn do_put(&self, stream: FlightDataStream) -> Result<DoPutResponseStream> {
828        let mut request = tonic::Request::new(stream);
829
830        if let Some(AuthHeader {
831            auth_scheme: Some(AuthScheme::Basic(Basic { username, password })),
832        }) = &self.ctx.auth_header
833        {
834            let encoded = BASE64_STANDARD.encode(format!("{username}:{password}"));
835            let value = MetadataValue::from_str(&format!("Basic {encoded}"))
836                .context(InvalidTonicMetadataValueSnafu)?;
837            request.metadata_mut().insert("x-greptime-auth", value);
838        }
839
840        let db_to_put = if !self.dbname.is_empty() {
841            &self.dbname
842        } else {
843            &build_db_string(self.catalog_or_default(), self.schema_or_default())
844        };
845        request.metadata_mut().insert(
846            "x-greptime-db-name",
847            MetadataValue::from_str(db_to_put).context(InvalidTonicMetadataValueSnafu)?,
848        );
849
850        let mut client = self.client.make_control_flight_client(false, false)?;
851        let response = client.mut_inner().do_put(request).await?;
852        let response = response
853            .into_inner()
854            .map_err(Into::into)
855            .and_then(|x| future::ready(DoPutResponse::try_from(x).context(ConvertFlightDataSnafu)))
856            .boxed();
857        Ok(response)
858    }
859}
860
861impl<'a> DatabaseFlightRequest<'a> {
862    /// Adds query optimization hints to this Flight request.
863    pub fn with_hints(mut self, hints: &[(&str, &str)]) -> Self {
864        self.options.hints = Database::encode_hints(hints);
865        self
866    }
867
868    /// Adds Flow extensions to this Flight request.
869    pub fn with_flow_extensions(mut self, flow_extensions: &[(&str, &str)]) -> Self {
870        self.options.flow_extensions = Database::encode_flow_extensions(flow_extensions);
871        self
872    }
873
874    /// Adds snapshot sequence fences to this Flight request.
875    pub fn with_snapshot_seqs(mut self, snapshot_seqs: &HashMap<u64, u64>) -> Self {
876        self.options.snapshot_seqs = Database::encode_snapshot_seqs(snapshot_seqs);
877        self
878    }
879
880    /// Sets a timeout for this Flight request only.
881    pub fn with_timeout(mut self, timeout: Duration) -> Self {
882        self.options.timeout = Some(timeout);
883        self
884    }
885
886    /// Executes a SQL query.
887    pub async fn sql<S>(self, sql: S) -> Result<Output>
888    where
889        S: AsRef<str>,
890    {
891        let request = Request::Query(QueryRequest {
892            query: Some(Query::Sql(sql.as_ref().to_string())),
893        });
894        self.do_get(request)
895            .await
896            .map(OutputWithMetrics::into_output)
897    }
898
899    /// Executes a SQL query and returns terminal metrics.
900    pub async fn sql_with_terminal_metrics<S>(self, sql: S) -> Result<OutputWithMetrics>
901    where
902        S: AsRef<str>,
903    {
904        self.query_with_terminal_metrics(QueryRequest {
905            query: Some(Query::Sql(sql.as_ref().to_string())),
906        })
907        .await
908    }
909
910    /// Executes a logical plan directly without SQL parsing.
911    pub async fn logical_plan(self, logical_plan: Vec<u8>) -> Result<Output> {
912        self.query_with_terminal_metrics(QueryRequest {
913            query: Some(Query::LogicalPlan(logical_plan)),
914        })
915        .await
916        .map(OutputWithMetrics::into_output)
917    }
918
919    /// Executes a query and returns terminal metrics.
920    pub async fn query_with_terminal_metrics(
921        self,
922        request: QueryRequest,
923    ) -> Result<OutputWithMetrics> {
924        self.do_get(Request::Query(request)).await
925    }
926
927    /// Creates a new table using the provided table expression.
928    #[cfg(feature = "testing")]
929    pub async fn create(self, expr: CreateTableExpr) -> Result<Output> {
930        self.do_get(Request::Ddl(DdlRequest {
931            expr: Some(DdlExpr::CreateTable(expr)),
932        }))
933        .await
934        .map(OutputWithMetrics::into_output)
935    }
936
937    /// Alters an existing table using the provided alter expression.
938    #[cfg(feature = "testing")]
939    pub async fn alter(self, expr: AlterTableExpr) -> Result<Output> {
940        self.do_get(Request::Ddl(DdlRequest {
941            expr: Some(DdlExpr::AlterTable(expr)),
942        }))
943        .await
944        .map(OutputWithMetrics::into_output)
945    }
946
947    async fn do_get(self, request: Request) -> Result<OutputWithMetrics> {
948        let Self { database, options } = self;
949        database.do_get(request, options).await
950    }
951}
952
953/// by grpc standard, only `Unavailable` is retryable, see: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc
954pub fn is_grpc_retryable(err: &tonic::Status) -> bool {
955    matches!(err.code(), tonic::Code::Unavailable)
956}
957
958#[derive(Default, Debug, Clone)]
959struct FlightContext {
960    auth_header: Option<AuthHeader>,
961}
962
963#[cfg(test)]
964mod tests {
965    use std::sync::Arc;
966    use std::task::{Context, Poll};
967
968    use api::v1::auth_header::AuthScheme;
969    use api::v1::{AuthHeader, Basic};
970    use common_error::ext::{ErrorExt, RetryHint};
971    use common_error::status_code::StatusCode;
972    use common_error::{GREPTIME_DB_HEADER_ERROR_CODE, GREPTIME_DB_HEADER_ERROR_RETRY_HINT};
973    use common_query::OutputData;
974    use common_recordbatch::{OrderOption, RecordBatch, RecordBatchStream};
975    use datatypes::prelude::{ConcreteDataType, VectorRef};
976    use datatypes::schema::{ColumnSchema, Schema};
977    use datatypes::vectors::Int32Vector;
978    use futures_util::StreamExt;
979    use tonic::codegen::http::{HeaderMap, HeaderValue};
980    use tonic::metadata::MetadataMap;
981    use tonic::{Code, Status};
982
983    use super::*;
984    use crate::error::TonicSnafu;
985
986    struct MockMetricsStream {
987        schema: datatypes::schema::SchemaRef,
988        batch: Option<RecordBatch>,
989        metrics: RecordBatchMetrics,
990        terminal_metrics_only: bool,
991    }
992
993    impl Stream for MockMetricsStream {
994        type Item = common_recordbatch::error::Result<RecordBatch>;
995
996        fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
997            Poll::Ready(self.batch.take().map(Ok))
998        }
999    }
1000
1001    impl RecordBatchStream for MockMetricsStream {
1002        fn name(&self) -> &str {
1003            "MockMetricsStream"
1004        }
1005
1006        fn schema(&self) -> datatypes::schema::SchemaRef {
1007            self.schema.clone()
1008        }
1009
1010        fn output_ordering(&self) -> Option<&[OrderOption]> {
1011            None
1012        }
1013
1014        fn metrics(&self) -> Option<RecordBatchMetrics> {
1015            if self.terminal_metrics_only && self.batch.is_some() {
1016                return None;
1017            }
1018            Some(self.metrics.clone())
1019        }
1020    }
1021
1022    fn terminal_metrics_json() -> String {
1023        terminal_metrics_json_with_seq(42)
1024    }
1025
1026    fn terminal_metrics_json_with_seq(seq: u64) -> String {
1027        serde_json::to_string(&RecordBatchMetrics {
1028            region_watermarks: vec![common_recordbatch::adapter::RegionWatermarkEntry {
1029                region_id: 7,
1030                watermark: Some(seq),
1031            }],
1032            ..Default::default()
1033        })
1034        .unwrap()
1035    }
1036
1037    #[test]
1038    fn test_put_flow_extensions_preserves_comma_bearing_values() {
1039        let mut metadata = MetadataMap::new();
1040        Database::put_metadata_value(
1041            &mut metadata,
1042            FLOW_EXTENSIONS_METADATA_KEY,
1043            Database::encode_flow_extensions(&[
1044                ("flow.return_region_seq", "true"),
1045                ("flow.incremental_after_seqs", r#"{"1":10,"2":20}"#),
1046            ])
1047            .unwrap(),
1048        )
1049        .unwrap();
1050
1051        let value = metadata
1052            .get(FLOW_EXTENSIONS_METADATA_KEY)
1053            .unwrap()
1054            .to_str()
1055            .unwrap();
1056        let decoded: Vec<(String, String)> = serde_json::from_str(value).unwrap();
1057        assert_eq!(
1058            decoded,
1059            vec![
1060                ("flow.return_region_seq".to_string(), "true".to_string()),
1061                (
1062                    "flow.incremental_after_seqs".to_string(),
1063                    r#"{"1":10,"2":20}"#.to_string()
1064                ),
1065            ]
1066        );
1067    }
1068
1069    #[test]
1070    fn test_put_snapshot_seqs_preserves_u64_precision() {
1071        let mut metadata = MetadataMap::new();
1072        let snapshot_seqs = std::collections::HashMap::from([
1073            (u64::MAX, u64::MAX - 1),
1074            (9_007_199_254_740_993_u64, 9_007_199_254_740_995_u64),
1075        ]);
1076
1077        Database::put_metadata_value(
1078            &mut metadata,
1079            SNAPSHOT_SEQS_METADATA_KEY,
1080            Database::encode_snapshot_seqs(&snapshot_seqs).unwrap(),
1081        )
1082        .unwrap();
1083
1084        let value = metadata
1085            .get(SNAPSHOT_SEQS_METADATA_KEY)
1086            .unwrap()
1087            .to_str()
1088            .unwrap();
1089        let decoded: std::collections::HashMap<u64, u64> = serde_json::from_str(value).unwrap();
1090        assert_eq!(decoded, snapshot_seqs);
1091    }
1092
1093    #[test]
1094    fn test_flight_request_builder_applies_request_options() {
1095        let database = Database::new("greptime", "public", Client::default());
1096        let snapshot_seqs = HashMap::from([(42, 99)]);
1097        let request = database
1098            .flight_request()
1099            .with_hints(&[("query_parallelism", "1")])
1100            .with_flow_extensions(&[("flow.return_region_seq", "true")])
1101            .with_snapshot_seqs(&snapshot_seqs)
1102            .with_timeout(Duration::from_millis(50));
1103        let mut tonic_request = tonic::Request::new(());
1104
1105        request.options.apply_to(&mut tonic_request).unwrap();
1106
1107        let metadata = tonic_request.metadata();
1108        assert_eq!(
1109            metadata.get(HINTS_METADATA_KEY).unwrap(),
1110            "query_parallelism=1"
1111        );
1112        assert_eq!(
1113            serde_json::from_str::<Vec<(String, String)>>(
1114                metadata
1115                    .get(FLOW_EXTENSIONS_METADATA_KEY)
1116                    .unwrap()
1117                    .to_str()
1118                    .unwrap(),
1119            )
1120            .unwrap(),
1121            vec![("flow.return_region_seq".to_string(), "true".to_string())]
1122        );
1123        assert_eq!(
1124            serde_json::from_str::<HashMap<u64, u64>>(
1125                metadata
1126                    .get(SNAPSHOT_SEQS_METADATA_KEY)
1127                    .unwrap()
1128                    .to_str()
1129                    .unwrap(),
1130            )
1131            .unwrap(),
1132            snapshot_seqs
1133        );
1134        assert!(metadata.get("grpc-timeout").is_some());
1135    }
1136
1137    #[test]
1138    fn test_flight_ctx() {
1139        let mut ctx = FlightContext::default();
1140        assert!(ctx.auth_header.is_none());
1141
1142        let basic = AuthScheme::Basic(Basic {
1143            username: "u".to_string(),
1144            password: "p".to_string(),
1145        });
1146
1147        ctx.auth_header = Some(AuthHeader {
1148            auth_scheme: Some(basic),
1149        });
1150
1151        assert!(matches!(
1152            ctx.auth_header,
1153            Some(AuthHeader {
1154                auth_scheme: Some(AuthScheme::Basic(_)),
1155            })
1156        ));
1157    }
1158
1159    #[test]
1160    fn test_from_tonic_status() {
1161        let expected = TonicSnafu {
1162            code: StatusCode::Internal,
1163            msg: "blabla".to_string(),
1164            tonic_code: Code::Internal,
1165            retry_hint: RetryHint::NonRetryable,
1166        }
1167        .build();
1168
1169        let status = Status::new(Code::Internal, "blabla");
1170        let actual: Error = status.into();
1171
1172        assert_eq!(expected.to_string(), actual.to_string());
1173        assert_eq!(expected.retry_hint(), actual.retry_hint());
1174        assert_eq!(expected.should_retry(), actual.should_retry());
1175    }
1176
1177    #[test]
1178    fn test_flight_stream_error_preserves_addr_and_message() {
1179        let error = flight_stream_error(
1180            "127.0.0.1:4001",
1181            Status::out_of_range("message length too large").into(),
1182        );
1183
1184        assert!(matches!(
1185            &error,
1186            Error::FlightStream {
1187                addr,
1188                tonic_code: Code::OutOfRange,
1189                message,
1190                ..
1191            } if addr == "127.0.0.1:4001" && message == "message length too large"
1192        ));
1193        assert_eq!(
1194            "Failed to receive Flight data from 127.0.0.1:4001, code: Operation was attempted past the valid range: message length too large",
1195            error.to_string(),
1196        );
1197    }
1198
1199    #[test]
1200    fn test_from_tonic_status_with_retry_hint() {
1201        let mut headers = HeaderMap::new();
1202        headers.insert(
1203            GREPTIME_DB_HEADER_ERROR_CODE,
1204            HeaderValue::from(StatusCode::Internal as u32),
1205        );
1206        headers.insert(
1207            GREPTIME_DB_HEADER_ERROR_RETRY_HINT,
1208            HeaderValue::from_static(RetryHint::Retryable.as_str()),
1209        );
1210        let status =
1211            Status::with_metadata(Code::Internal, "blabla", MetadataMap::from_headers(headers));
1212
1213        let actual: Error = status.into();
1214
1215        assert_eq!(actual.retry_hint(), RetryHint::Retryable);
1216        assert!(actual.should_retry());
1217    }
1218
1219    #[test]
1220    fn test_from_tonic_status_fallback() {
1221        let mut headers = HeaderMap::new();
1222        headers.insert(
1223            GREPTIME_DB_HEADER_ERROR_CODE,
1224            HeaderValue::from(StatusCode::InvalidArguments as u32),
1225        );
1226        let status =
1227            Status::with_metadata(Code::Internal, "blabla", MetadataMap::from_headers(headers));
1228
1229        let actual: Error = status.into();
1230
1231        assert_eq!(actual.retry_hint(), RetryHint::NonRetryable);
1232        assert!(!actual.should_retry());
1233    }
1234
1235    #[test]
1236    fn test_should_retry_preserves_transport_retry() {
1237        let status = Status::new(Code::Unavailable, "blabla");
1238        let actual: Error = status.into();
1239
1240        assert!(actual.should_retry());
1241    }
1242
1243    #[tokio::test]
1244    async fn test_query_with_terminal_metrics_tracks_terminal_only_metrics() {
1245        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1246            "v",
1247            ConcreteDataType::int32_datatype(),
1248            false,
1249        )]));
1250        let batch = RecordBatch::new(
1251            schema.clone(),
1252            vec![Arc::new(Int32Vector::from_slice([1, 2])) as VectorRef],
1253        )
1254        .unwrap();
1255        let output = Output::new_with_stream(Box::pin(MockMetricsStream {
1256            schema,
1257            batch: Some(batch),
1258            metrics: RecordBatchMetrics {
1259                region_watermarks: vec![common_recordbatch::adapter::RegionWatermarkEntry {
1260                    region_id: 7,
1261                    watermark: Some(42),
1262                }],
1263                ..Default::default()
1264            },
1265            terminal_metrics_only: true,
1266        }));
1267
1268        let result = OutputWithMetrics::from_output(output);
1269        let terminal_metrics = result.metrics.clone();
1270        assert!(!terminal_metrics.is_ready());
1271        assert!(terminal_metrics.get().is_none());
1272
1273        let OutputData::Stream(mut stream) = result.output.data else {
1274            panic!("expected stream output");
1275        };
1276        while stream.next().await.is_some() {}
1277
1278        assert!(terminal_metrics.is_ready());
1279        assert_eq!(
1280            terminal_metrics.participating_regions(),
1281            Some(std::collections::BTreeSet::from([7_u64]))
1282        );
1283        assert_eq!(
1284            terminal_metrics.region_watermark_map(),
1285            Some(std::collections::HashMap::from([(7_u64, 42_u64)]))
1286        );
1287    }
1288
1289    #[test]
1290    fn test_parse_terminal_metrics_rejects_invalid_json() {
1291        assert!(parse_terminal_metrics("{not-json}").is_err());
1292    }
1293
1294    #[tokio::test]
1295    async fn test_affected_rows_inline_metrics_are_parsed() {
1296        let output = output_from_flight_message_stream(
1297            "test-peer".to_string(),
1298            futures_util::stream::iter(vec![Ok(FlightMessage::AffectedRows {
1299                rows: 3,
1300                metrics: Some(terminal_metrics_json()),
1301            })] as Vec<Result<FlightMessage>>),
1302        )
1303        .await
1304        .unwrap();
1305
1306        assert!(matches!(output.output.data, OutputData::AffectedRows(3)));
1307        assert!(output.metrics.is_ready());
1308        assert_eq!(
1309            output.metrics.region_watermark_map(),
1310            Some(std::collections::HashMap::from([(7, 42)]))
1311        );
1312    }
1313
1314    #[tokio::test]
1315    async fn test_affected_rows_inline_metrics_rejects_trailing_metrics() {
1316        let metrics_json = terminal_metrics_json();
1317        let err = output_from_flight_message_stream(
1318            "test-peer".to_string(),
1319            futures_util::stream::iter(vec![
1320                Ok(FlightMessage::AffectedRows {
1321                    rows: 3,
1322                    metrics: Some(metrics_json.clone()),
1323                }),
1324                Ok(FlightMessage::Metrics(metrics_json)),
1325            ] as Vec<Result<FlightMessage>>),
1326        )
1327        .await
1328        .unwrap_err();
1329
1330        assert!(
1331            err.to_string().contains("already carries Metrics"),
1332            "unexpected error: {err:?}"
1333        );
1334    }
1335
1336    #[tokio::test]
1337    async fn test_invalid_terminal_metrics_after_record_batch_yields_batch_then_error() {
1338        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1339            "v",
1340            ConcreteDataType::int32_datatype(),
1341            false,
1342        )]));
1343        let batch = RecordBatch::new(
1344            schema.clone(),
1345            vec![Arc::new(Int32Vector::from_slice([1])) as VectorRef],
1346        )
1347        .unwrap();
1348        let output = output_from_flight_message_stream(
1349            "test-peer".to_string(),
1350            futures_util::stream::iter(vec![
1351                Ok(FlightMessage::Schema(schema.arrow_schema().clone())),
1352                Ok(FlightMessage::RecordBatch(batch.into_df_record_batch())),
1353                Ok(FlightMessage::Metrics("{not-json}".to_string())),
1354            ] as Vec<Result<FlightMessage>>),
1355        )
1356        .await
1357        .unwrap();
1358        let terminal_metrics = output.metrics.clone();
1359        let OutputData::Stream(mut record_batch_stream) = output.output.data else {
1360            panic!("expected stream output");
1361        };
1362
1363        let batch = record_batch_stream.next().await.unwrap().unwrap();
1364        assert_eq!(batch.num_rows(), 1);
1365
1366        let err = record_batch_stream.next().await.unwrap().unwrap_err();
1367        assert_eq!("External error", err.to_string());
1368        assert!(
1369            format!("{err:?}").contains("Invalid terminal metrics message"),
1370            "unexpected error: {err:?}"
1371        );
1372        assert!(record_batch_stream.next().await.is_none());
1373        assert!(terminal_metrics.is_ready());
1374        assert!(terminal_metrics.get().is_none());
1375    }
1376
1377    #[tokio::test]
1378    async fn test_record_batch_stream_continues_after_partial_metrics() {
1379        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1380            "v",
1381            ConcreteDataType::int32_datatype(),
1382            false,
1383        )]));
1384        let first_batch = RecordBatch::new(
1385            schema.clone(),
1386            vec![Arc::new(Int32Vector::from_slice([1])) as VectorRef],
1387        )
1388        .unwrap();
1389        let second_batch = RecordBatch::new(
1390            schema.clone(),
1391            vec![Arc::new(Int32Vector::from_slice([2])) as VectorRef],
1392        )
1393        .unwrap();
1394        let output = output_from_flight_message_stream(
1395            "test-peer".to_string(),
1396            futures_util::stream::iter(vec![
1397                Ok(FlightMessage::Schema(schema.arrow_schema().clone())),
1398                Ok(FlightMessage::RecordBatch(
1399                    first_batch.into_df_record_batch(),
1400                )),
1401                Ok(FlightMessage::Metrics(terminal_metrics_json_with_seq(1))),
1402                Ok(FlightMessage::RecordBatch(
1403                    second_batch.into_df_record_batch(),
1404                )),
1405                Ok(FlightMessage::Metrics(terminal_metrics_json_with_seq(2))),
1406            ] as Vec<Result<FlightMessage>>),
1407        )
1408        .await
1409        .unwrap();
1410        let terminal_metrics = output.metrics.clone();
1411        let OutputData::Stream(mut record_batch_stream) = output.output.data else {
1412            panic!("expected stream output");
1413        };
1414
1415        let first_batch = record_batch_stream.next().await.unwrap().unwrap();
1416        assert_eq!(first_batch.num_rows(), 1);
1417        let second_batch = record_batch_stream.next().await.unwrap().unwrap();
1418        assert_eq!(second_batch.num_rows(), 1);
1419        assert!(record_batch_stream.next().await.is_none());
1420
1421        assert!(terminal_metrics.is_ready());
1422        assert_eq!(
1423            terminal_metrics.region_watermark_map(),
1424            Some(std::collections::HashMap::from([(7, 2)]))
1425        );
1426    }
1427
1428    #[test]
1429    fn test_output_metrics_distinguishes_empty_region_watermarks_from_absence() {
1430        let metrics = OutputMetrics::default();
1431        metrics.update(Some(RecordBatchMetrics::default()));
1432
1433        assert_eq!(
1434            metrics.participating_regions(),
1435            Some(std::collections::BTreeSet::new())
1436        );
1437        assert_eq!(
1438            metrics.region_watermark_map(),
1439            Some(std::collections::HashMap::new())
1440        );
1441    }
1442}