Skip to main content

servers/grpc/
flight.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod stream;
16
17use std::collections::HashMap;
18use std::pin::Pin;
19use std::sync::Arc;
20use std::task::{Context, Poll};
21
22use api::v1::GreptimeRequest;
23use arrow_flight::flight_service_server::FlightService;
24use arrow_flight::{
25    Action, ActionType, Criteria, Empty, FlightData, FlightDescriptor, FlightInfo,
26    HandshakeRequest, HandshakeResponse, PollInfo, PutResult, SchemaResult, Ticket,
27};
28use async_trait::async_trait;
29use bytes::{self, Bytes};
30use common_error::ext::ErrorExt;
31use common_grpc::flight::do_put::{DoPutMetadata, DoPutResponse};
32use common_grpc::flight::{
33    FLOW_EXTENSIONS_METADATA_KEY, FlightDecoder, FlightMessage, SNAPSHOT_SEQS_METADATA_KEY,
34};
35use common_memory_manager::MemoryGuard;
36use common_query::{Output, OutputData};
37use common_recordbatch::DfRecordBatch;
38use common_telemetry::debug;
39use common_telemetry::tracing::info_span;
40use common_telemetry::tracing_context::{FutureExt, TracingContext};
41use datatypes::arrow::datatypes::SchemaRef;
42use futures::{Stream, future, ready};
43use futures_util::{StreamExt, TryStreamExt};
44use prost::Message;
45use query::metrics::terminal_recordbatch_metrics_from_plan_if_requested;
46use query::options::FlowQueryExtensions;
47use session::context::{Channel, QueryContextRef};
48use snafu::{IntoError, OptionExt, ResultExt, ensure};
49use table::table_name::TableName;
50use tokio::sync::mpsc;
51use tokio_stream::wrappers::ReceiverStream;
52use tonic::{Request, Response, Status, Streaming};
53
54use crate::error::{InvalidParameterSnafu, InvalidQuerySnafu, Result, ToJsonSnafu};
55pub use crate::grpc::flight::stream::{
56    FlightRecordBatchSource, FlightRecordBatchStream, FlightRecordBatchStreamInput,
57};
58use crate::grpc::greptime_handler::{
59    GreptimeRequestHandler, create_query_context, get_request_type,
60};
61use crate::grpc::{FlightCompression, TonicResult, context_auth};
62use crate::request_memory_limiter::ServerMemoryLimiter;
63use crate::request_memory_metrics::RequestMemoryMetrics;
64use crate::{error, hint_headers};
65
66pub type TonicStream<T> = Pin<Box<dyn Stream<Item = TonicResult<T>> + Send + 'static>>;
67
68/// A subset of [FlightService]
69#[async_trait]
70pub trait FlightCraft: Send + Sync + 'static {
71    async fn do_get(
72        &self,
73        request: Request<Ticket>,
74    ) -> TonicResult<Response<TonicStream<FlightData>>>;
75
76    async fn do_put(
77        &self,
78        request: Request<Streaming<FlightData>>,
79    ) -> TonicResult<Response<TonicStream<PutResult>>> {
80        let _ = request;
81        Err(Status::unimplemented("Not yet implemented"))
82    }
83}
84
85pub type FlightCraftRef = Arc<dyn FlightCraft>;
86
87pub struct FlightCraftWrapper<T: FlightCraft>(pub T);
88
89impl<T: FlightCraft> From<T> for FlightCraftWrapper<T> {
90    fn from(t: T) -> Self {
91        Self(t)
92    }
93}
94
95#[async_trait]
96impl FlightCraft for FlightCraftRef {
97    async fn do_get(
98        &self,
99        request: Request<Ticket>,
100    ) -> TonicResult<Response<TonicStream<FlightData>>> {
101        (**self).do_get(request).await
102    }
103
104    async fn do_put(
105        &self,
106        request: Request<Streaming<FlightData>>,
107    ) -> TonicResult<Response<TonicStream<PutResult>>> {
108        self.as_ref().do_put(request).await
109    }
110}
111
112#[async_trait]
113impl<T: FlightCraft> FlightService for FlightCraftWrapper<T> {
114    type HandshakeStream = TonicStream<HandshakeResponse>;
115
116    async fn handshake(
117        &self,
118        _: Request<Streaming<HandshakeRequest>>,
119    ) -> TonicResult<Response<Self::HandshakeStream>> {
120        Err(Status::unimplemented("Not yet implemented"))
121    }
122
123    type ListFlightsStream = TonicStream<FlightInfo>;
124
125    async fn list_flights(
126        &self,
127        _: Request<Criteria>,
128    ) -> TonicResult<Response<Self::ListFlightsStream>> {
129        Err(Status::unimplemented("Not yet implemented"))
130    }
131
132    async fn get_flight_info(
133        &self,
134        _: Request<FlightDescriptor>,
135    ) -> TonicResult<Response<FlightInfo>> {
136        Err(Status::unimplemented("Not yet implemented"))
137    }
138
139    async fn poll_flight_info(
140        &self,
141        _: Request<FlightDescriptor>,
142    ) -> TonicResult<Response<PollInfo>> {
143        Err(Status::unimplemented("Not yet implemented"))
144    }
145
146    async fn get_schema(
147        &self,
148        _: Request<FlightDescriptor>,
149    ) -> TonicResult<Response<SchemaResult>> {
150        Err(Status::unimplemented("Not yet implemented"))
151    }
152
153    type DoGetStream = TonicStream<FlightData>;
154
155    async fn do_get(&self, request: Request<Ticket>) -> TonicResult<Response<Self::DoGetStream>> {
156        self.0.do_get(request).await
157    }
158
159    type DoPutStream = TonicStream<PutResult>;
160
161    async fn do_put(
162        &self,
163        request: Request<Streaming<FlightData>>,
164    ) -> TonicResult<Response<Self::DoPutStream>> {
165        self.0.do_put(request).await
166    }
167
168    type DoExchangeStream = TonicStream<FlightData>;
169
170    async fn do_exchange(
171        &self,
172        _: Request<Streaming<FlightData>>,
173    ) -> TonicResult<Response<Self::DoExchangeStream>> {
174        Err(Status::unimplemented("Not yet implemented"))
175    }
176
177    type DoActionStream = TonicStream<arrow_flight::Result>;
178
179    async fn do_action(&self, _: Request<Action>) -> TonicResult<Response<Self::DoActionStream>> {
180        Err(Status::unimplemented("Not yet implemented"))
181    }
182
183    type ListActionsStream = TonicStream<ActionType>;
184
185    async fn list_actions(
186        &self,
187        _: Request<Empty>,
188    ) -> TonicResult<Response<Self::ListActionsStream>> {
189        Err(Status::unimplemented("Not yet implemented"))
190    }
191}
192
193#[async_trait]
194impl FlightCraft for GreptimeRequestHandler {
195    async fn do_get(
196        &self,
197        request: Request<Ticket>,
198    ) -> TonicResult<Response<TonicStream<FlightData>>> {
199        let mut hints = hint_headers::extract_hints(request.metadata());
200        hints.extend(extract_flow_extensions(request.metadata())?);
201        let snapshot_seqs = extract_snapshot_seqs(request.metadata())?;
202        let channel = request
203            .extensions()
204            .get::<Channel>()
205            .copied()
206            .unwrap_or(Channel::Grpc);
207
208        let ticket = request.into_inner().ticket;
209        let request =
210            GreptimeRequest::decode(ticket.as_ref()).context(error::InvalidFlightTicketSnafu)?;
211        let query_ctx =
212            create_query_context(channel, request.header.as_ref(), hints, snapshot_seqs)?;
213        // Validate flow hint syntax at the transport boundary before dispatching the request.
214        // This does not authorize or execute anything; `handle_request()` below still performs
215        // the normal frontend handling and auth checks before query execution.
216        let flow_extensions = FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())
217            .map_err(|e| Status::invalid_argument(e.output_msg()))?;
218        let should_emit_terminal_metrics = flow_extensions
219            .as_ref()
220            .is_some_and(|extensions| extensions.should_collect_region_watermark());
221
222        // The Grpc protocol pass query by Flight. It needs to be wrapped under a span, in order to record stream
223        let span = info_span!(
224            "GreptimeRequestHandler::do_get",
225            protocol = "grpc",
226            request_type = get_request_type(&request)
227        );
228        let flight_compression = self.flight_compression;
229        async {
230            let query = request.request.context(InvalidQuerySnafu {
231                reason: "Expecting non-empty GreptimeRequest.",
232            })?;
233            self.authenticate_request_with_query_ctx(request.header.as_ref(), &query_ctx)
234                .await?;
235            let output = self.handle_request_with_query_ctx(query, query_ctx.clone());
236            let stream = to_flight_data_stream(
237                output,
238                TracingContext::from_current_span(),
239                flight_compression,
240                query_ctx,
241                should_emit_terminal_metrics,
242            );
243            Ok(Response::new(stream))
244        }
245        .trace(span)
246        .await
247    }
248
249    async fn do_put(
250        &self,
251        request: Request<Streaming<FlightData>>,
252    ) -> TonicResult<Response<TonicStream<PutResult>>> {
253        let (headers, extensions, stream) = request.into_parts();
254
255        let limiter = extensions.get::<ServerMemoryLimiter>().cloned();
256
257        let query_ctx =
258            context_auth::create_query_context_from_grpc_metadata(&headers, &extensions)?;
259        context_auth::check_auth(self.user_provider.clone(), &headers, query_ctx.clone()).await?;
260
261        const MAX_PENDING_RESPONSES: usize = 32;
262        let (tx, rx) = mpsc::channel::<TonicResult<DoPutResponse>>(MAX_PENDING_RESPONSES);
263
264        let stream = PutRecordBatchRequestStream::new(
265            stream,
266            query_ctx.current_catalog().to_string(),
267            query_ctx.current_schema(),
268            limiter,
269        )
270        .await?;
271        // Ack immediately when stream is created successfully (in Init state)
272        let _ = tx.send(Ok(DoPutResponse::new(0, 0, 0.0))).await;
273        self.put_record_batches(stream, tx, query_ctx).await;
274
275        let response = ReceiverStream::new(rx)
276            .and_then(|response| {
277                future::ready({
278                    serde_json::to_vec(&response)
279                        .context(ToJsonSnafu)
280                        .map(|x| PutResult {
281                            app_metadata: Bytes::from(x),
282                        })
283                        .map_err(Into::into)
284                })
285            })
286            .boxed();
287        Ok(Response::new(response))
288    }
289}
290
291pub struct PutRecordBatchRequest {
292    pub table_name: TableName,
293    pub request_id: i64,
294    pub timestamp_range: Option<(i64, i64)>,
295    pub record_batch: DfRecordBatch,
296    pub schema_bytes: Bytes,
297    pub flight_data: FlightData,
298    pub(crate) _guard: Option<MemoryGuard<RequestMemoryMetrics>>,
299}
300
301impl PutRecordBatchRequest {
302    fn try_new(
303        table_name: TableName,
304        record_batch: DfRecordBatch,
305        request_id: i64,
306        timestamp_range: Option<(i64, i64)>,
307        schema_bytes: Bytes,
308        flight_data: FlightData,
309        limiter: Option<&ServerMemoryLimiter>,
310    ) -> Result<Self> {
311        let memory_usage = flight_data.data_body.len()
312            + flight_data.app_metadata.len()
313            + flight_data.data_header.len();
314
315        let _guard = if let Some(limiter) = limiter {
316            let guard = limiter.try_acquire(memory_usage as u64).ok_or_else(|| {
317                let inner_err = common_memory_manager::Error::MemoryLimitExceeded {
318                    requested_bytes: memory_usage as u64,
319                    limit_bytes: limiter.limit_bytes(),
320                };
321                error::MemoryLimitExceededSnafu.into_error(inner_err)
322            })?;
323            Some(guard)
324        } else {
325            None
326        };
327
328        Ok(Self {
329            table_name,
330            request_id,
331            timestamp_range,
332            record_batch,
333            schema_bytes,
334            flight_data,
335            _guard,
336        })
337    }
338}
339
340pub struct PutRecordBatchRequestStream {
341    flight_data_stream: Streaming<FlightData>,
342    catalog: String,
343    schema_name: String,
344    limiter: Option<ServerMemoryLimiter>,
345    // Client now lazily sends schema data so we cannot eagerly wait for it.
346    // Instead, we need to decode while receiving record batches.
347    state: StreamState,
348}
349
350enum StreamState {
351    Init,
352    Ready {
353        table_name: TableName,
354        schema: SchemaRef,
355        schema_bytes: Bytes,
356        decoder: FlightDecoder,
357    },
358}
359
360impl PutRecordBatchRequestStream {
361    /// Creates a new `PutRecordBatchRequestStream` in Init state.
362    /// The stream will transition to Ready state when it receives the schema message.
363    pub async fn new(
364        flight_data_stream: Streaming<FlightData>,
365        catalog: String,
366        schema: String,
367        limiter: Option<ServerMemoryLimiter>,
368    ) -> TonicResult<Self> {
369        Ok(Self {
370            flight_data_stream,
371            catalog,
372            schema_name: schema,
373            limiter,
374            state: StreamState::Init,
375        })
376    }
377
378    /// Returns the table name extracted from the flight descriptor.
379    /// Returns None if the stream is still in Init state.
380    pub fn table_name(&self) -> Option<&TableName> {
381        match &self.state {
382            StreamState::Init => None,
383            StreamState::Ready { table_name, .. } => Some(table_name),
384        }
385    }
386
387    /// Returns the Arrow schema decoded from the first flight message.
388    /// Returns None if the stream is still in Init state.
389    pub fn schema(&self) -> Option<&SchemaRef> {
390        match &self.state {
391            StreamState::Init => None,
392            StreamState::Ready { schema, .. } => Some(schema),
393        }
394    }
395
396    /// Returns the raw schema bytes in IPC format.
397    /// Returns None if the stream is still in Init state.
398    pub fn schema_bytes(&self) -> Option<&Bytes> {
399        match &self.state {
400            StreamState::Init => None,
401            StreamState::Ready { schema_bytes, .. } => Some(schema_bytes),
402        }
403    }
404
405    fn extract_table_name(mut descriptor: FlightDescriptor) -> Result<String> {
406        ensure!(
407            descriptor.r#type == arrow_flight::flight_descriptor::DescriptorType::Path as i32,
408            InvalidParameterSnafu {
409                reason: "expect FlightDescriptor::type == 'Path' only",
410            }
411        );
412        ensure!(
413            descriptor.path.len() == 1,
414            InvalidParameterSnafu {
415                reason: "expect FlightDescriptor::path has only one table name",
416            }
417        );
418        Ok(descriptor.path.remove(0))
419    }
420}
421
422impl Stream for PutRecordBatchRequestStream {
423    type Item = TonicResult<PutRecordBatchRequest>;
424
425    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
426        loop {
427            let poll = ready!(self.flight_data_stream.poll_next_unpin(cx));
428
429            match poll {
430                Some(Ok(flight_data)) => {
431                    let limiter = self.limiter.clone();
432
433                    match &mut self.state {
434                        StreamState::Init => {
435                            // First message - expecting schema
436                            let flight_descriptor = match flight_data.flight_descriptor.as_ref() {
437                                Some(descriptor) => descriptor.clone(),
438                                None => {
439                                    return Poll::Ready(Some(Err(Status::failed_precondition(
440                                        "table to put is not found in flight descriptor",
441                                    ))));
442                                }
443                            };
444
445                            let table_name_str = match Self::extract_table_name(flight_descriptor) {
446                                Ok(name) => name,
447                                Err(e) => {
448                                    return Poll::Ready(Some(Err(Status::invalid_argument(
449                                        e.to_string(),
450                                    ))));
451                                }
452                            };
453                            let table_name = TableName::new(
454                                self.catalog.clone(),
455                                self.schema_name.clone(),
456                                table_name_str,
457                            );
458
459                            // Decode the schema
460                            let mut decoder = FlightDecoder::default();
461                            let schema_message = decoder.try_decode(&flight_data).map_err(|e| {
462                                Status::invalid_argument(format!("Failed to decode schema: {}", e))
463                            })?;
464
465                            match schema_message {
466                                Some(FlightMessage::Schema(schema)) => {
467                                    let schema_bytes = decoder.schema_bytes().ok_or_else(|| {
468                                        Status::internal(
469                                            "decoder should have schema bytes after decoding schema",
470                                        )
471                                    })?;
472
473                                    // Transition to Ready state with all necessary data
474                                    self.state = StreamState::Ready {
475                                        table_name,
476                                        schema,
477                                        schema_bytes,
478                                        decoder,
479                                    };
480                                    // Continue to next iteration to process RecordBatch messages
481                                    continue;
482                                }
483                                _ => {
484                                    return Poll::Ready(Some(Err(Status::failed_precondition(
485                                        "first message must be a Schema message",
486                                    ))));
487                                }
488                            }
489                        }
490                        StreamState::Ready {
491                            table_name,
492                            schema: _,
493                            schema_bytes,
494                            decoder,
495                        } => {
496                            // Extract request_id and time range from FlightData before decoding
497                            let metadata = if !flight_data.app_metadata.is_empty() {
498                                serde_json::from_slice::<DoPutMetadata>(&flight_data.app_metadata)
499                                    .ok()
500                            } else {
501                                None
502                            };
503                            let request_id = metadata
504                                .as_ref()
505                                .map(|meta| meta.request_id())
506                                .unwrap_or_default();
507                            let timestamp_range = metadata.and_then(|meta| meta.timestamp_range());
508
509                            // Decode FlightData to RecordBatch
510                            match decoder.try_decode(&flight_data) {
511                                Ok(Some(FlightMessage::RecordBatch(record_batch))) => {
512                                    let table_name = table_name.clone();
513                                    let schema_bytes = schema_bytes.clone();
514                                    return Poll::Ready(Some(
515                                        PutRecordBatchRequest::try_new(
516                                            table_name,
517                                            record_batch,
518                                            request_id,
519                                            timestamp_range,
520                                            schema_bytes,
521                                            flight_data,
522                                            limiter.as_ref(),
523                                        )
524                                        .map_err(|e| Status::invalid_argument(e.to_string())),
525                                    ));
526                                }
527                                Ok(Some(other)) => {
528                                    debug!("Unexpected flight message: {:?}", other);
529                                    return Poll::Ready(Some(Err(Status::invalid_argument(
530                                        "Expected RecordBatch message, got other message type",
531                                    ))));
532                                }
533                                Ok(None) => {
534                                    // Dictionary batch - processed internally by decoder, continue polling
535                                    continue;
536                                }
537                                Err(e) => {
538                                    return Poll::Ready(Some(Err(Status::invalid_argument(
539                                        format!("Failed to decode RecordBatch: {}", e),
540                                    ))));
541                                }
542                            }
543                        }
544                    }
545                }
546                Some(Err(e)) => {
547                    return Poll::Ready(Some(Err(e)));
548                }
549                None => {
550                    return Poll::Ready(None);
551                }
552            }
553        }
554    }
555}
556
557fn extract_flow_extensions(
558    metadata: &tonic::metadata::MetadataMap,
559) -> TonicResult<Vec<(String, String)>> {
560    Ok(extract_json_metadata(metadata, FLOW_EXTENSIONS_METADATA_KEY)?.unwrap_or_default())
561}
562
563fn extract_snapshot_seqs(
564    metadata: &tonic::metadata::MetadataMap,
565) -> TonicResult<HashMap<u64, u64>> {
566    Ok(extract_json_metadata(metadata, SNAPSHOT_SEQS_METADATA_KEY)?.unwrap_or_default())
567}
568
569fn extract_json_metadata<T: serde::de::DeserializeOwned>(
570    metadata: &tonic::metadata::MetadataMap,
571    key: &'static str,
572) -> TonicResult<Option<T>> {
573    let Some(value) = metadata.get(key) else {
574        return Ok(None);
575    };
576
577    let value = value
578        .to_str()
579        .map_err(|e| Status::invalid_argument(format!("Invalid {key} metadata value: {e}")))?;
580
581    let parsed = serde_json::from_str::<T>(value)
582        .map_err(|e| Status::invalid_argument(format!("Invalid {key} metadata JSON: {e}")))?;
583    Ok(Some(parsed))
584}
585
586fn to_flight_data_stream<F>(
587    output: F,
588    tracing_context: TracingContext,
589    flight_compression: FlightCompression,
590    query_ctx: QueryContextRef,
591    should_emit_terminal_metrics: bool,
592) -> TonicStream<FlightData>
593where
594    F: std::future::Future<Output = Result<Output>> + Send + 'static,
595{
596    let initializer = async move {
597        let output = output.await.map_err(Status::from)?;
598        output_to_flight_record_batch_source(output, should_emit_terminal_metrics)
599    };
600    let stream = FlightRecordBatchStream::new(
601        FlightRecordBatchStreamInput::initializer(initializer),
602        tracing_context,
603        flight_compression,
604        query_ctx,
605    );
606    Box::pin(stream) as _
607}
608
609fn output_to_flight_record_batch_source(
610    output: Output,
611    should_emit_terminal_metrics: bool,
612) -> TonicResult<FlightRecordBatchSource> {
613    match output.data {
614        OutputData::Stream(stream) => Ok(FlightRecordBatchSource::RecordBatches(stream)),
615        OutputData::RecordBatches(x) => Ok(FlightRecordBatchSource::RecordBatches(x.as_stream())),
616        OutputData::AffectedRows(rows) => {
617            let terminal_metrics = match terminal_recordbatch_metrics_from_plan_if_requested(
618                output.meta.plan,
619                should_emit_terminal_metrics,
620            ) {
621                Some(metrics) => match serde_json::to_string(&metrics) {
622                    Ok(metrics) => Some(metrics),
623                    Err(e) => {
624                        return Err(Status::internal(format!(
625                            "Failed to serialize terminal metrics: {e}"
626                        )));
627                    }
628                },
629                None => None,
630            };
631            Ok(FlightRecordBatchSource::AffectedRows {
632                rows,
633                metrics: terminal_metrics,
634            })
635        }
636    }
637}
638
639#[cfg(test)]
640mod tests {
641    use query::options::{
642        FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, FLOW_RETURN_REGION_SEQ,
643        FLOW_SCHEDULED_TIME_MILLIS, FLOW_SINK_TABLE_ID, FlowIncrementalMode,
644    };
645    use tonic::metadata::{AsciiMetadataValue, MetadataMap};
646
647    use super::*;
648
649    #[test]
650    fn test_extract_flow_extensions_preserves_comma_bearing_values() {
651        let mut metadata = MetadataMap::new();
652        metadata.insert(
653            FLOW_EXTENSIONS_METADATA_KEY,
654            AsciiMetadataValue::try_from(
655                r#"[["flow.return_region_seq","true"],["flow.incremental_after_seqs","{\"1\":10,\"2\":20}"]]"#,
656            )
657            .unwrap(),
658        );
659
660        let extensions = extract_flow_extensions(&metadata).unwrap();
661        assert_eq!(
662            extensions,
663            vec![
664                ("flow.return_region_seq".to_string(), "true".to_string()),
665                (
666                    "flow.incremental_after_seqs".to_string(),
667                    r#"{"1":10,"2":20}"#.to_string()
668                ),
669            ]
670        );
671    }
672
673    #[test]
674    fn test_flow_extensions_can_carry_scheduled_time() {
675        let mut metadata = MetadataMap::new();
676        metadata.insert(
677            FLOW_EXTENSIONS_METADATA_KEY,
678            AsciiMetadataValue::try_from(r#"[["flow.scheduled_time_millis","1700000000000"]]"#)
679                .unwrap(),
680        );
681
682        let flow_extensions = extract_flow_extensions(&metadata).unwrap();
683        let query_ctx =
684            create_query_context(Channel::Grpc, None, flow_extensions, HashMap::new()).unwrap();
685
686        assert_eq!(
687            query_ctx.extension(FLOW_SCHEDULED_TIME_MILLIS),
688            Some("1700000000000")
689        );
690    }
691
692    #[test]
693    fn test_flow_extensions_forward_sequence_range_to_query_context() {
694        let mut metadata = MetadataMap::new();
695        metadata.insert(
696            FLOW_EXTENSIONS_METADATA_KEY,
697            AsciiMetadataValue::try_from(
698                r#"[["flow.return_region_seq","true"],["flow.incremental_mode","sequence_range"],["flow.incremental_after_seqs","{\"1\":10,\"2\":20}"],["flow.sink_table_id","42"]]"#,
699            )
700            .unwrap(),
701        );
702
703        let flow_extensions = extract_flow_extensions(&metadata).unwrap();
704        let query_ctx =
705            create_query_context(Channel::Grpc, None, flow_extensions, HashMap::new()).unwrap();
706        let parsed =
707            query::options::FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())
708                .unwrap()
709                .unwrap();
710
711        assert_eq!(
712            parsed.incremental_mode,
713            Some(FlowIncrementalMode::SequenceRange)
714        );
715        assert_eq!(
716            parsed.incremental_after_seqs,
717            Some(HashMap::from([(1, 10), (2, 20)]))
718        );
719        assert!(parsed.return_region_seq);
720        assert_eq!(parsed.sink_table_id, Some(42));
721        assert_eq!(
722            query_ctx.extension(FLOW_INCREMENTAL_MODE),
723            Some("sequence_range")
724        );
725        assert_eq!(
726            query_ctx.extension(FLOW_INCREMENTAL_AFTER_SEQS),
727            Some(r#"{"1":10,"2":20}"#)
728        );
729        assert_eq!(query_ctx.extension(FLOW_RETURN_REGION_SEQ), Some("true"));
730        assert_eq!(query_ctx.extension(FLOW_SINK_TABLE_ID), Some("42"));
731    }
732
733    #[test]
734    fn test_extract_flow_extensions_rejects_invalid_json() {
735        let mut metadata = MetadataMap::new();
736        metadata.insert(
737            FLOW_EXTENSIONS_METADATA_KEY,
738            AsciiMetadataValue::try_from("not-json").unwrap(),
739        );
740
741        let err = extract_flow_extensions(&metadata).unwrap_err();
742        assert_eq!(err.code(), tonic::Code::InvalidArgument);
743    }
744}