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