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        self.do_put_with_hints(stream, &[]).await
829    }
830
831    /// Ingest a stream of [RecordBatch]es using Arrow Flight's `DoPut` with request hints.
832    pub async fn do_put_with_hints(
833        &self,
834        stream: FlightDataStream,
835        hints: &[(&str, &str)],
836    ) -> Result<DoPutResponseStream> {
837        let mut request = tonic::Request::new(stream);
838        Self::put_hints(request.metadata_mut(), hints)?;
839
840        if let Some(AuthHeader {
841            auth_scheme: Some(AuthScheme::Basic(Basic { username, password })),
842        }) = &self.ctx.auth_header
843        {
844            let encoded = BASE64_STANDARD.encode(format!("{username}:{password}"));
845            let value = MetadataValue::from_str(&format!("Basic {encoded}"))
846                .context(InvalidTonicMetadataValueSnafu)?;
847            request.metadata_mut().insert("x-greptime-auth", value);
848        }
849
850        let db_to_put = if !self.dbname.is_empty() {
851            &self.dbname
852        } else {
853            &build_db_string(self.catalog_or_default(), self.schema_or_default())
854        };
855        request.metadata_mut().insert(
856            "x-greptime-db-name",
857            MetadataValue::from_str(db_to_put).context(InvalidTonicMetadataValueSnafu)?,
858        );
859
860        let mut client = self.client.make_control_flight_client(false, false)?;
861        let response = client.mut_inner().do_put(request).await?;
862        let response = response
863            .into_inner()
864            .map_err(Into::into)
865            .and_then(|x| future::ready(DoPutResponse::try_from(x).context(ConvertFlightDataSnafu)))
866            .boxed();
867        Ok(response)
868    }
869}
870
871impl<'a> DatabaseFlightRequest<'a> {
872    /// Adds query optimization hints to this Flight request.
873    pub fn with_hints(mut self, hints: &[(&str, &str)]) -> Self {
874        self.options.hints = Database::encode_hints(hints);
875        self
876    }
877
878    /// Adds Flow extensions to this Flight request.
879    pub fn with_flow_extensions(mut self, flow_extensions: &[(&str, &str)]) -> Self {
880        self.options.flow_extensions = Database::encode_flow_extensions(flow_extensions);
881        self
882    }
883
884    /// Adds snapshot sequence fences to this Flight request.
885    pub fn with_snapshot_seqs(mut self, snapshot_seqs: &HashMap<u64, u64>) -> Self {
886        self.options.snapshot_seqs = Database::encode_snapshot_seqs(snapshot_seqs);
887        self
888    }
889
890    /// Sets a timeout for this Flight request only.
891    pub fn with_timeout(mut self, timeout: Duration) -> Self {
892        self.options.timeout = Some(timeout);
893        self
894    }
895
896    /// Executes a SQL query.
897    pub async fn sql<S>(self, sql: S) -> Result<Output>
898    where
899        S: AsRef<str>,
900    {
901        let request = Request::Query(QueryRequest {
902            query: Some(Query::Sql(sql.as_ref().to_string())),
903        });
904        self.do_get(request)
905            .await
906            .map(OutputWithMetrics::into_output)
907    }
908
909    /// Executes a SQL query and returns terminal metrics.
910    pub async fn sql_with_terminal_metrics<S>(self, sql: S) -> Result<OutputWithMetrics>
911    where
912        S: AsRef<str>,
913    {
914        self.query_with_terminal_metrics(QueryRequest {
915            query: Some(Query::Sql(sql.as_ref().to_string())),
916        })
917        .await
918    }
919
920    /// Executes a logical plan directly without SQL parsing.
921    pub async fn logical_plan(self, logical_plan: Vec<u8>) -> Result<Output> {
922        self.query_with_terminal_metrics(QueryRequest {
923            query: Some(Query::LogicalPlan(logical_plan)),
924        })
925        .await
926        .map(OutputWithMetrics::into_output)
927    }
928
929    /// Executes a query and returns terminal metrics.
930    pub async fn query_with_terminal_metrics(
931        self,
932        request: QueryRequest,
933    ) -> Result<OutputWithMetrics> {
934        self.do_get(Request::Query(request)).await
935    }
936
937    /// Creates a new table using the provided table expression.
938    #[cfg(feature = "testing")]
939    pub async fn create(self, expr: CreateTableExpr) -> Result<Output> {
940        self.do_get(Request::Ddl(DdlRequest {
941            expr: Some(DdlExpr::CreateTable(expr)),
942        }))
943        .await
944        .map(OutputWithMetrics::into_output)
945    }
946
947    /// Alters an existing table using the provided alter expression.
948    #[cfg(feature = "testing")]
949    pub async fn alter(self, expr: AlterTableExpr) -> Result<Output> {
950        self.do_get(Request::Ddl(DdlRequest {
951            expr: Some(DdlExpr::AlterTable(expr)),
952        }))
953        .await
954        .map(OutputWithMetrics::into_output)
955    }
956
957    async fn do_get(self, request: Request) -> Result<OutputWithMetrics> {
958        let Self { database, options } = self;
959        database.do_get(request, options).await
960    }
961}
962
963/// 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
964pub fn is_grpc_retryable(err: &tonic::Status) -> bool {
965    matches!(err.code(), tonic::Code::Unavailable)
966}
967
968#[derive(Default, Debug, Clone)]
969struct FlightContext {
970    auth_header: Option<AuthHeader>,
971}
972
973#[cfg(test)]
974mod tests {
975    use std::sync::Arc;
976    use std::task::{Context, Poll};
977
978    use api::v1::auth_header::AuthScheme;
979    use api::v1::{AuthHeader, Basic};
980    use common_error::ext::{ErrorExt, RetryHint};
981    use common_error::status_code::StatusCode;
982    use common_error::{GREPTIME_DB_HEADER_ERROR_CODE, GREPTIME_DB_HEADER_ERROR_RETRY_HINT};
983    use common_query::OutputData;
984    use common_recordbatch::{OrderOption, RecordBatch, RecordBatchStream};
985    use datatypes::prelude::{ConcreteDataType, VectorRef};
986    use datatypes::schema::{ColumnSchema, Schema};
987    use datatypes::vectors::Int32Vector;
988    use futures_util::StreamExt;
989    use tonic::codegen::http::{HeaderMap, HeaderValue};
990    use tonic::metadata::MetadataMap;
991    use tonic::{Code, Status};
992
993    use super::*;
994    use crate::error::TonicSnafu;
995
996    struct MockMetricsStream {
997        schema: datatypes::schema::SchemaRef,
998        batch: Option<RecordBatch>,
999        metrics: RecordBatchMetrics,
1000        terminal_metrics_only: bool,
1001    }
1002
1003    impl Stream for MockMetricsStream {
1004        type Item = common_recordbatch::error::Result<RecordBatch>;
1005
1006        fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1007            Poll::Ready(self.batch.take().map(Ok))
1008        }
1009    }
1010
1011    impl RecordBatchStream for MockMetricsStream {
1012        fn name(&self) -> &str {
1013            "MockMetricsStream"
1014        }
1015
1016        fn schema(&self) -> datatypes::schema::SchemaRef {
1017            self.schema.clone()
1018        }
1019
1020        fn output_ordering(&self) -> Option<&[OrderOption]> {
1021            None
1022        }
1023
1024        fn metrics(&self) -> Option<RecordBatchMetrics> {
1025            if self.terminal_metrics_only && self.batch.is_some() {
1026                return None;
1027            }
1028            Some(self.metrics.clone())
1029        }
1030    }
1031
1032    fn terminal_metrics_json() -> String {
1033        terminal_metrics_json_with_seq(42)
1034    }
1035
1036    fn terminal_metrics_json_with_seq(seq: u64) -> String {
1037        serde_json::to_string(&RecordBatchMetrics {
1038            region_watermarks: vec![common_recordbatch::adapter::RegionWatermarkEntry {
1039                region_id: 7,
1040                watermark: Some(seq),
1041            }],
1042            ..Default::default()
1043        })
1044        .unwrap()
1045    }
1046
1047    #[test]
1048    fn test_put_flow_extensions_preserves_comma_bearing_values() {
1049        let mut metadata = MetadataMap::new();
1050        Database::put_metadata_value(
1051            &mut metadata,
1052            FLOW_EXTENSIONS_METADATA_KEY,
1053            Database::encode_flow_extensions(&[
1054                ("flow.return_region_seq", "true"),
1055                ("flow.incremental_after_seqs", r#"{"1":10,"2":20}"#),
1056            ])
1057            .unwrap(),
1058        )
1059        .unwrap();
1060
1061        let value = metadata
1062            .get(FLOW_EXTENSIONS_METADATA_KEY)
1063            .unwrap()
1064            .to_str()
1065            .unwrap();
1066        let decoded: Vec<(String, String)> = serde_json::from_str(value).unwrap();
1067        assert_eq!(
1068            decoded,
1069            vec![
1070                ("flow.return_region_seq".to_string(), "true".to_string()),
1071                (
1072                    "flow.incremental_after_seqs".to_string(),
1073                    r#"{"1":10,"2":20}"#.to_string()
1074                ),
1075            ]
1076        );
1077    }
1078
1079    #[test]
1080    fn test_put_snapshot_seqs_preserves_u64_precision() {
1081        let mut metadata = MetadataMap::new();
1082        let snapshot_seqs = std::collections::HashMap::from([
1083            (u64::MAX, u64::MAX - 1),
1084            (9_007_199_254_740_993_u64, 9_007_199_254_740_995_u64),
1085        ]);
1086
1087        Database::put_metadata_value(
1088            &mut metadata,
1089            SNAPSHOT_SEQS_METADATA_KEY,
1090            Database::encode_snapshot_seqs(&snapshot_seqs).unwrap(),
1091        )
1092        .unwrap();
1093
1094        let value = metadata
1095            .get(SNAPSHOT_SEQS_METADATA_KEY)
1096            .unwrap()
1097            .to_str()
1098            .unwrap();
1099        let decoded: std::collections::HashMap<u64, u64> = serde_json::from_str(value).unwrap();
1100        assert_eq!(decoded, snapshot_seqs);
1101    }
1102
1103    #[test]
1104    fn test_flight_request_builder_applies_request_options() {
1105        let database = Database::new("greptime", "public", Client::default());
1106        let snapshot_seqs = HashMap::from([(42, 99)]);
1107        let request = database
1108            .flight_request()
1109            .with_hints(&[("query_parallelism", "1")])
1110            .with_flow_extensions(&[("flow.return_region_seq", "true")])
1111            .with_snapshot_seqs(&snapshot_seqs)
1112            .with_timeout(Duration::from_millis(50));
1113        let mut tonic_request = tonic::Request::new(());
1114
1115        request.options.apply_to(&mut tonic_request).unwrap();
1116
1117        let metadata = tonic_request.metadata();
1118        assert_eq!(
1119            metadata.get(HINTS_METADATA_KEY).unwrap(),
1120            "query_parallelism=1"
1121        );
1122        assert_eq!(
1123            serde_json::from_str::<Vec<(String, String)>>(
1124                metadata
1125                    .get(FLOW_EXTENSIONS_METADATA_KEY)
1126                    .unwrap()
1127                    .to_str()
1128                    .unwrap(),
1129            )
1130            .unwrap(),
1131            vec![("flow.return_region_seq".to_string(), "true".to_string())]
1132        );
1133        assert_eq!(
1134            serde_json::from_str::<HashMap<u64, u64>>(
1135                metadata
1136                    .get(SNAPSHOT_SEQS_METADATA_KEY)
1137                    .unwrap()
1138                    .to_str()
1139                    .unwrap(),
1140            )
1141            .unwrap(),
1142            snapshot_seqs
1143        );
1144        assert!(metadata.get("grpc-timeout").is_some());
1145    }
1146
1147    #[test]
1148    fn test_flight_ctx() {
1149        let mut ctx = FlightContext::default();
1150        assert!(ctx.auth_header.is_none());
1151
1152        let basic = AuthScheme::Basic(Basic {
1153            username: "u".to_string(),
1154            password: "p".to_string(),
1155        });
1156
1157        ctx.auth_header = Some(AuthHeader {
1158            auth_scheme: Some(basic),
1159        });
1160
1161        assert!(matches!(
1162            ctx.auth_header,
1163            Some(AuthHeader {
1164                auth_scheme: Some(AuthScheme::Basic(_)),
1165            })
1166        ));
1167    }
1168
1169    #[test]
1170    fn test_from_tonic_status() {
1171        let expected = TonicSnafu {
1172            code: StatusCode::Internal,
1173            msg: "blabla".to_string(),
1174            tonic_code: Code::Internal,
1175            retry_hint: RetryHint::NonRetryable,
1176        }
1177        .build();
1178
1179        let status = Status::new(Code::Internal, "blabla");
1180        let actual: Error = status.into();
1181
1182        assert_eq!(expected.to_string(), actual.to_string());
1183        assert_eq!(expected.retry_hint(), actual.retry_hint());
1184        assert_eq!(expected.should_retry(), actual.should_retry());
1185    }
1186
1187    #[test]
1188    fn test_flight_stream_error_preserves_addr_and_message() {
1189        let error = flight_stream_error(
1190            "127.0.0.1:4001",
1191            Status::out_of_range("message length too large").into(),
1192        );
1193
1194        assert!(matches!(
1195            &error,
1196            Error::FlightStream {
1197                addr,
1198                tonic_code: Code::OutOfRange,
1199                message,
1200                ..
1201            } if addr == "127.0.0.1:4001" && message == "message length too large"
1202        ));
1203        assert_eq!(
1204            "Failed to receive Flight data from 127.0.0.1:4001, code: Operation was attempted past the valid range: message length too large",
1205            error.to_string(),
1206        );
1207    }
1208
1209    #[test]
1210    fn test_from_tonic_status_with_retry_hint() {
1211        let mut headers = HeaderMap::new();
1212        headers.insert(
1213            GREPTIME_DB_HEADER_ERROR_CODE,
1214            HeaderValue::from(StatusCode::Internal as u32),
1215        );
1216        headers.insert(
1217            GREPTIME_DB_HEADER_ERROR_RETRY_HINT,
1218            HeaderValue::from_static(RetryHint::Retryable.as_str()),
1219        );
1220        let status =
1221            Status::with_metadata(Code::Internal, "blabla", MetadataMap::from_headers(headers));
1222
1223        let actual: Error = status.into();
1224
1225        assert_eq!(actual.retry_hint(), RetryHint::Retryable);
1226        assert!(actual.should_retry());
1227    }
1228
1229    #[test]
1230    fn test_from_tonic_status_fallback() {
1231        let mut headers = HeaderMap::new();
1232        headers.insert(
1233            GREPTIME_DB_HEADER_ERROR_CODE,
1234            HeaderValue::from(StatusCode::InvalidArguments as u32),
1235        );
1236        let status =
1237            Status::with_metadata(Code::Internal, "blabla", MetadataMap::from_headers(headers));
1238
1239        let actual: Error = status.into();
1240
1241        assert_eq!(actual.retry_hint(), RetryHint::NonRetryable);
1242        assert!(!actual.should_retry());
1243    }
1244
1245    #[test]
1246    fn test_should_retry_preserves_transport_retry() {
1247        let status = Status::new(Code::Unavailable, "blabla");
1248        let actual: Error = status.into();
1249
1250        assert!(actual.should_retry());
1251    }
1252
1253    #[tokio::test]
1254    async fn test_query_with_terminal_metrics_tracks_terminal_only_metrics() {
1255        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1256            "v",
1257            ConcreteDataType::int32_datatype(),
1258            false,
1259        )]));
1260        let batch = RecordBatch::new(
1261            schema.clone(),
1262            vec![Arc::new(Int32Vector::from_slice([1, 2])) as VectorRef],
1263        )
1264        .unwrap();
1265        let output = Output::new_with_stream(Box::pin(MockMetricsStream {
1266            schema,
1267            batch: Some(batch),
1268            metrics: RecordBatchMetrics {
1269                region_watermarks: vec![common_recordbatch::adapter::RegionWatermarkEntry {
1270                    region_id: 7,
1271                    watermark: Some(42),
1272                }],
1273                ..Default::default()
1274            },
1275            terminal_metrics_only: true,
1276        }));
1277
1278        let result = OutputWithMetrics::from_output(output);
1279        let terminal_metrics = result.metrics.clone();
1280        assert!(!terminal_metrics.is_ready());
1281        assert!(terminal_metrics.get().is_none());
1282
1283        let OutputData::Stream(mut stream) = result.output.data else {
1284            panic!("expected stream output");
1285        };
1286        while stream.next().await.is_some() {}
1287
1288        assert!(terminal_metrics.is_ready());
1289        assert_eq!(
1290            terminal_metrics.participating_regions(),
1291            Some(std::collections::BTreeSet::from([7_u64]))
1292        );
1293        assert_eq!(
1294            terminal_metrics.region_watermark_map(),
1295            Some(std::collections::HashMap::from([(7_u64, 42_u64)]))
1296        );
1297    }
1298
1299    #[test]
1300    fn test_parse_terminal_metrics_rejects_invalid_json() {
1301        assert!(parse_terminal_metrics("{not-json}").is_err());
1302    }
1303
1304    #[tokio::test]
1305    async fn test_affected_rows_inline_metrics_are_parsed() {
1306        let output = output_from_flight_message_stream(
1307            "test-peer".to_string(),
1308            futures_util::stream::iter(vec![Ok(FlightMessage::AffectedRows {
1309                rows: 3,
1310                metrics: Some(terminal_metrics_json()),
1311            })] as Vec<Result<FlightMessage>>),
1312        )
1313        .await
1314        .unwrap();
1315
1316        assert!(matches!(output.output.data, OutputData::AffectedRows(3)));
1317        assert!(output.metrics.is_ready());
1318        assert_eq!(
1319            output.metrics.region_watermark_map(),
1320            Some(std::collections::HashMap::from([(7, 42)]))
1321        );
1322    }
1323
1324    #[tokio::test]
1325    async fn test_affected_rows_inline_metrics_rejects_trailing_metrics() {
1326        let metrics_json = terminal_metrics_json();
1327        let err = output_from_flight_message_stream(
1328            "test-peer".to_string(),
1329            futures_util::stream::iter(vec![
1330                Ok(FlightMessage::AffectedRows {
1331                    rows: 3,
1332                    metrics: Some(metrics_json.clone()),
1333                }),
1334                Ok(FlightMessage::Metrics(metrics_json)),
1335            ] as Vec<Result<FlightMessage>>),
1336        )
1337        .await
1338        .unwrap_err();
1339
1340        assert!(
1341            err.to_string().contains("already carries Metrics"),
1342            "unexpected error: {err:?}"
1343        );
1344    }
1345
1346    #[tokio::test]
1347    async fn test_invalid_terminal_metrics_after_record_batch_yields_batch_then_error() {
1348        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1349            "v",
1350            ConcreteDataType::int32_datatype(),
1351            false,
1352        )]));
1353        let batch = RecordBatch::new(
1354            schema.clone(),
1355            vec![Arc::new(Int32Vector::from_slice([1])) as VectorRef],
1356        )
1357        .unwrap();
1358        let output = output_from_flight_message_stream(
1359            "test-peer".to_string(),
1360            futures_util::stream::iter(vec![
1361                Ok(FlightMessage::Schema(schema.arrow_schema().clone())),
1362                Ok(FlightMessage::RecordBatch(batch.into_df_record_batch())),
1363                Ok(FlightMessage::Metrics("{not-json}".to_string())),
1364            ] as Vec<Result<FlightMessage>>),
1365        )
1366        .await
1367        .unwrap();
1368        let terminal_metrics = output.metrics.clone();
1369        let OutputData::Stream(mut record_batch_stream) = output.output.data else {
1370            panic!("expected stream output");
1371        };
1372
1373        let batch = record_batch_stream.next().await.unwrap().unwrap();
1374        assert_eq!(batch.num_rows(), 1);
1375
1376        let err = record_batch_stream.next().await.unwrap().unwrap_err();
1377        assert_eq!("External error", err.to_string());
1378        assert!(
1379            format!("{err:?}").contains("Invalid terminal metrics message"),
1380            "unexpected error: {err:?}"
1381        );
1382        assert!(record_batch_stream.next().await.is_none());
1383        assert!(terminal_metrics.is_ready());
1384        assert!(terminal_metrics.get().is_none());
1385    }
1386
1387    #[tokio::test]
1388    async fn test_record_batch_stream_continues_after_partial_metrics() {
1389        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1390            "v",
1391            ConcreteDataType::int32_datatype(),
1392            false,
1393        )]));
1394        let first_batch = RecordBatch::new(
1395            schema.clone(),
1396            vec![Arc::new(Int32Vector::from_slice([1])) as VectorRef],
1397        )
1398        .unwrap();
1399        let second_batch = RecordBatch::new(
1400            schema.clone(),
1401            vec![Arc::new(Int32Vector::from_slice([2])) as VectorRef],
1402        )
1403        .unwrap();
1404        let output = output_from_flight_message_stream(
1405            "test-peer".to_string(),
1406            futures_util::stream::iter(vec![
1407                Ok(FlightMessage::Schema(schema.arrow_schema().clone())),
1408                Ok(FlightMessage::RecordBatch(
1409                    first_batch.into_df_record_batch(),
1410                )),
1411                Ok(FlightMessage::Metrics(terminal_metrics_json_with_seq(1))),
1412                Ok(FlightMessage::RecordBatch(
1413                    second_batch.into_df_record_batch(),
1414                )),
1415                Ok(FlightMessage::Metrics(terminal_metrics_json_with_seq(2))),
1416            ] as Vec<Result<FlightMessage>>),
1417        )
1418        .await
1419        .unwrap();
1420        let terminal_metrics = output.metrics.clone();
1421        let OutputData::Stream(mut record_batch_stream) = output.output.data else {
1422            panic!("expected stream output");
1423        };
1424
1425        let first_batch = record_batch_stream.next().await.unwrap().unwrap();
1426        assert_eq!(first_batch.num_rows(), 1);
1427        let second_batch = record_batch_stream.next().await.unwrap().unwrap();
1428        assert_eq!(second_batch.num_rows(), 1);
1429        assert!(record_batch_stream.next().await.is_none());
1430
1431        assert!(terminal_metrics.is_ready());
1432        assert_eq!(
1433            terminal_metrics.region_watermark_map(),
1434            Some(std::collections::HashMap::from([(7, 2)]))
1435        );
1436    }
1437
1438    #[test]
1439    fn test_output_metrics_distinguishes_empty_region_watermarks_from_absence() {
1440        let metrics = OutputMetrics::default();
1441        metrics.update(Some(RecordBatchMetrics::default()));
1442
1443        assert_eq!(
1444            metrics.participating_regions(),
1445            Some(std::collections::BTreeSet::new())
1446        );
1447        assert_eq!(
1448            metrics.region_watermark_map(),
1449            Some(std::collections::HashMap::new())
1450        );
1451    }
1452}