Skip to main content

query/
dummy_catalog.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
15//! Dummy catalog for region server.
16
17use std::any::Any;
18use std::collections::HashMap;
19use std::fmt;
20use std::sync::{Arc, Mutex};
21
22use api::v1::SemanticType;
23use async_trait::async_trait;
24use catalog::error::Result as CatalogResult;
25use catalog::{CatalogManager, CatalogManagerRef};
26use common_recordbatch::OrderOption;
27use common_recordbatch::filter::SimpleFilterEvaluator;
28use datafusion::catalog::{CatalogProvider, CatalogProviderList, SchemaProvider, Session};
29use datafusion::datasource::TableProvider;
30use datafusion::physical_plan::ExecutionPlan;
31use datafusion_common::DataFusionError;
32use datafusion_expr::{Expr, TableProviderFilterPushDown, TableType};
33use datatypes::arrow::datatypes::SchemaRef;
34use datatypes::types::json_type::JsonNativeType;
35use futures::stream::BoxStream;
36use session::context::{QueryContext, QueryContextRef};
37use snafu::ResultExt;
38use store_api::metadata::RegionMetadataRef;
39use store_api::region_engine::RegionEngineRef;
40use store_api::storage::{
41    RegionId, ScanRequest, TimeSeriesDistribution, TimeSeriesRowSelector, VectorSearchRequest,
42};
43use table::TableRef;
44use table::metadata::{TableId, TableInfoRef};
45use table::table::scan::RegionScanExec;
46
47use crate::error::{GetRegionMetadataSnafu, Result};
48use crate::options::{FlowIncrementalMode, FlowQueryExtensions};
49
50/// Resolve to the given region (specified by [RegionId]) unconditionally.
51#[derive(Clone, Debug)]
52pub struct DummyCatalogList {
53    catalog: DummyCatalogProvider,
54}
55
56impl DummyCatalogList {
57    /// Creates a new catalog list with the given table provider.
58    pub fn with_table_provider(table_provider: Arc<dyn TableProvider>) -> Self {
59        let schema_provider = DummySchemaProvider {
60            table: table_provider,
61        };
62        let catalog_provider = DummyCatalogProvider {
63            schema: schema_provider,
64        };
65        Self {
66            catalog: catalog_provider,
67        }
68    }
69}
70
71impl CatalogProviderList for DummyCatalogList {
72    fn as_any(&self) -> &dyn Any {
73        self
74    }
75
76    fn register_catalog(
77        &self,
78        _name: String,
79        _catalog: Arc<dyn CatalogProvider>,
80    ) -> Option<Arc<dyn CatalogProvider>> {
81        None
82    }
83
84    fn catalog_names(&self) -> Vec<String> {
85        vec![]
86    }
87
88    fn catalog(&self, _name: &str) -> Option<Arc<dyn CatalogProvider>> {
89        Some(Arc::new(self.catalog.clone()))
90    }
91}
92
93/// A dummy catalog provider for [DummyCatalogList].
94#[derive(Clone, Debug)]
95struct DummyCatalogProvider {
96    schema: DummySchemaProvider,
97}
98
99impl CatalogProvider for DummyCatalogProvider {
100    fn as_any(&self) -> &dyn Any {
101        self
102    }
103
104    fn schema_names(&self) -> Vec<String> {
105        vec![]
106    }
107
108    fn schema(&self, _name: &str) -> Option<Arc<dyn SchemaProvider>> {
109        Some(Arc::new(self.schema.clone()))
110    }
111}
112
113/// A dummy schema provider for [DummyCatalogList].
114#[derive(Clone, Debug)]
115struct DummySchemaProvider {
116    table: Arc<dyn TableProvider>,
117}
118
119#[async_trait]
120impl SchemaProvider for DummySchemaProvider {
121    fn as_any(&self) -> &dyn Any {
122        self
123    }
124
125    fn table_names(&self) -> Vec<String> {
126        vec![]
127    }
128
129    async fn table(
130        &self,
131        _name: &str,
132    ) -> datafusion::error::Result<Option<Arc<dyn TableProvider>>> {
133        Ok(Some(self.table.clone()))
134    }
135
136    fn table_exist(&self, _name: &str) -> bool {
137        true
138    }
139}
140
141/// For [TableProvider] and [DummyCatalogList]
142#[derive(Clone)]
143pub struct DummyTableProvider {
144    region_id: RegionId,
145    engine: RegionEngineRef,
146    metadata: RegionMetadataRef,
147    /// Keeping a mutable request makes it possible to change in the optimize phase.
148    scan_request: Arc<Mutex<ScanRequest>>,
149    query_ctx: Option<QueryContextRef>,
150}
151
152impl fmt::Debug for DummyTableProvider {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        f.debug_struct("DummyTableProvider")
155            .field("region_id", &self.region_id)
156            .field("metadata", &self.metadata)
157            .field("scan_request", &self.scan_request)
158            .finish()
159    }
160}
161
162#[async_trait]
163impl TableProvider for DummyTableProvider {
164    fn as_any(&self) -> &dyn Any {
165        self
166    }
167
168    fn schema(&self) -> SchemaRef {
169        self.metadata.schema.arrow_schema().clone()
170    }
171
172    fn table_type(&self) -> TableType {
173        TableType::Base
174    }
175
176    async fn scan(
177        &self,
178        _state: &dyn Session,
179        projection: Option<&Vec<usize>>,
180        filters: &[Expr],
181        limit: Option<usize>,
182    ) -> datafusion::error::Result<Arc<dyn ExecutionPlan>> {
183        let mut request = self.scan_request.lock().unwrap().clone();
184        request.projection_input = projection.map(|p| p.clone().into());
185        request.filters = filters.to_vec();
186        request.limit = limit;
187        if let Some(query_ctx) = &self.query_ctx {
188            let is_sink_scan = is_sink_scan(query_ctx, self.region_id)
189                .map_err(|e| DataFusionError::External(Box::new(e)))?;
190            apply_cached_snapshot_to_request(query_ctx, self.region_id, is_sink_scan, &mut request);
191        }
192
193        let scanner = self
194            .engine
195            .handle_query(self.region_id, request.clone())
196            .await
197            .map_err(|e| DataFusionError::External(Box::new(e)))?;
198
199        if request.snapshot_on_scan
200            && let Some(query_ctx) = &self.query_ctx
201            && let Some(snapshot_sequence) = scanner.snapshot_sequence()
202        {
203            bind_snapshot_bound_region_seq(query_ctx, self.region_id, snapshot_sequence)
204                .map_err(|e| DataFusionError::External(Box::new(e)))?;
205        }
206
207        let query_memory_tracker = self.engine.query_memory_tracker();
208        let mut scan_exec = RegionScanExec::new(scanner, request, query_memory_tracker)?;
209        if let Some(query_ctx) = &self.query_ctx {
210            scan_exec.set_explain_verbose(query_ctx.explain_verbose());
211        }
212        Ok(Arc::new(scan_exec))
213    }
214
215    fn supports_filters_pushdown(
216        &self,
217        filters: &[&Expr],
218    ) -> datafusion::error::Result<Vec<TableProviderFilterPushDown>> {
219        let supported = filters
220            .iter()
221            .map(|e| {
222                // Simple filter on primary key columns are precisely evaluated.
223                if let Some(simple_filter) = SimpleFilterEvaluator::try_new(e) {
224                    if self
225                        .metadata
226                        .column_by_name(simple_filter.column_name())
227                        .and_then(|c| {
228                            (c.semantic_type == SemanticType::Tag
229                                || c.semantic_type == SemanticType::Timestamp)
230                                .then_some(())
231                        })
232                        .is_some()
233                    {
234                        TableProviderFilterPushDown::Exact
235                    } else {
236                        TableProviderFilterPushDown::Inexact
237                    }
238                } else {
239                    TableProviderFilterPushDown::Inexact
240                }
241            })
242            .collect();
243        Ok(supported)
244    }
245}
246
247impl DummyTableProvider {
248    /// Creates a new provider.
249    pub fn new(region_id: RegionId, engine: RegionEngineRef, metadata: RegionMetadataRef) -> Self {
250        Self {
251            region_id,
252            engine,
253            metadata,
254            scan_request: Default::default(),
255            query_ctx: None,
256        }
257    }
258
259    pub fn region_metadata(&self) -> RegionMetadataRef {
260        self.metadata.clone()
261    }
262
263    /// Sets the ordering hint of the query to the provider.
264    pub fn with_ordering_hint(&self, order_opts: &[OrderOption]) {
265        self.scan_request.lock().unwrap().output_ordering = Some(order_opts.to_vec());
266    }
267
268    /// Sets the distribution hint of the query to the provider.
269    pub fn with_distribution(&self, distribution: TimeSeriesDistribution) {
270        self.scan_request.lock().unwrap().distribution = Some(distribution);
271    }
272
273    /// Sets the time series selector hint of the query to the provider.
274    pub fn with_time_series_selector_hint(&self, selector: TimeSeriesRowSelector) {
275        self.scan_request.lock().unwrap().series_row_selector = Some(selector);
276    }
277
278    pub fn with_vector_search_hint(&self, hint: VectorSearchRequest) {
279        self.scan_request.lock().unwrap().vector_search = Some(hint);
280    }
281
282    pub fn get_vector_search_hint(&self) -> Option<VectorSearchRequest> {
283        self.scan_request.lock().unwrap().vector_search.clone()
284    }
285
286    pub fn with_sequence(&self, sequence: u64) {
287        self.scan_request.lock().unwrap().memtable_max_sequence = Some(sequence);
288    }
289
290    pub(crate) fn with_json_type_hint(&self, hint: HashMap<String, JsonNativeType>) {
291        self.scan_request.lock().unwrap().json_type_hint = hint;
292    }
293
294    /// Gets the scan request of the provider.
295    #[cfg(test)]
296    pub fn scan_request(&self) -> ScanRequest {
297        self.scan_request.lock().unwrap().clone()
298    }
299}
300
301pub struct DummyTableProviderFactory;
302
303impl DummyTableProviderFactory {
304    pub async fn create_table_provider(
305        &self,
306        region_id: RegionId,
307        engine: RegionEngineRef,
308        query_ctx: Option<QueryContextRef>,
309    ) -> Result<DummyTableProvider> {
310        let metadata =
311            engine
312                .get_metadata(region_id)
313                .await
314                .with_context(|_| GetRegionMetadataSnafu {
315                    engine: engine.name(),
316                    region_id,
317                })?;
318
319        let scan_request = if let Some(ctx) = query_ctx.as_ref() {
320            scan_request_from_query_context(region_id, ctx)?
321        } else {
322            ScanRequest::default()
323        };
324
325        Ok(DummyTableProvider {
326            region_id,
327            engine,
328            metadata,
329            scan_request: Arc::new(Mutex::new(scan_request)),
330            query_ctx,
331        })
332    }
333}
334
335fn scan_request_from_query_context(
336    region_id: RegionId,
337    query_ctx: &QueryContext,
338) -> Result<ScanRequest> {
339    let decision = decide_flow_scan(query_ctx, region_id)?;
340    Ok(build_scan_request(query_ctx, region_id, &decision))
341}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
344struct FlowScanDecision {
345    /// Whether this region is the flow sink-table scan.
346    /// Sink scans intentionally bypass incremental and snapshot-binding semantics.
347    is_sink_scan: bool,
348    /// Whether this scan should bind a memtable upper bound when opening the scan.
349    /// This is only the initial intent; if a cached bound already exists in `query_ctx`,
350    /// we reuse that cached bound instead and clear this flag.
351    snapshot_on_scan: bool,
352    /// Optional lower exclusive memtable sequence bound for incremental reads.
353    /// When set, only rows with sequence strictly greater than this bound are read from memtables.
354    memtable_min_sequence: Option<u64>,
355    /// Optional cached per-region snapshot already bound in `query_ctx`.
356    /// When present, this becomes the effective memtable upper bound and suppresses
357    /// binding a new snapshot on scan open.
358    memtable_max_sequence: Option<u64>,
359    /// Whether to skip SST files for memtable-only incremental source scans.
360    skip_sst_files: bool,
361}
362
363impl FlowScanDecision {
364    fn plain_scan() -> Self {
365        Self {
366            is_sink_scan: true,
367            snapshot_on_scan: false,
368            memtable_min_sequence: None,
369            memtable_max_sequence: None,
370            skip_sst_files: false,
371        }
372    }
373}
374
375fn decide_flow_scan(query_ctx: &QueryContext, region_id: RegionId) -> Result<FlowScanDecision> {
376    let Some(flow_extensions) =
377        FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())?
378    else {
379        return Ok(FlowScanDecision {
380            is_sink_scan: false,
381            snapshot_on_scan: false,
382            memtable_min_sequence: None,
383            memtable_max_sequence: query_ctx.get_snapshot(region_id.as_u64()),
384            skip_sst_files: false,
385        });
386    };
387
388    // Sink-table scans intentionally bypass all flow scan semantics. They should
389    // behave like plain reads and must not participate in incremental lower bounds
390    // or per-region snapshot binding/reuse.
391    if flow_extensions.sink_table_id == Some(region_id.table_id()) {
392        return Ok(FlowScanDecision::plain_scan());
393    }
394
395    let apply_incremental = flow_extensions.validate_for_scan(region_id)?;
396
397    let memtable_min_sequence = if apply_incremental {
398        flow_extensions
399            .incremental_after_seqs
400            .as_ref()
401            .and_then(|seqs| seqs.get(&region_id.as_u64()))
402            .copied()
403    } else {
404        None
405    };
406
407    let memtable_max_sequence = query_ctx.get_snapshot(region_id.as_u64());
408
409    // `skip_sst_files` is only valid for memtable-only incremental deltas,
410    // identified by a lower checkpoint bound. A snapshot upper bound without an
411    // incremental lower bound is a fenced full-snapshot read and must keep SSTs
412    // in the scan so mito can reject stale upper bounds after H has been flushed
413    // into SSTs, instead of silently bypassing the stale-fence check. If a future
414    // incremental delta also carries an upper bound, the lower-bound stale check
415    // still proves whether memtable-only is safe.
416    let skip_sst_files = apply_incremental
417        && memtable_min_sequence.is_some()
418        && flow_extensions.incremental_mode == Some(FlowIncrementalMode::MemtableOnly);
419
420    Ok(FlowScanDecision {
421        is_sink_scan: false,
422        snapshot_on_scan: memtable_max_sequence.is_none()
423            && flow_extensions.should_collect_region_watermark(),
424        memtable_min_sequence,
425        memtable_max_sequence,
426        skip_sst_files,
427    })
428}
429
430fn build_scan_request(
431    query_ctx: &QueryContext,
432    region_id: RegionId,
433    decision: &FlowScanDecision,
434) -> ScanRequest {
435    // Build the initial scan request from the final decision known at provider creation
436    // time. A later scan may still refresh `memtable_max_sequence` if another source scan
437    // has bound a snapshot into `query_ctx` after this provider was created.
438    ScanRequest {
439        sst_min_sequence: (!decision.is_sink_scan)
440            .then(|| query_ctx.sst_min_sequence(region_id.as_u64()))
441            .flatten(),
442        skip_sst_files: decision.skip_sst_files,
443        snapshot_on_scan: decision.snapshot_on_scan,
444        memtable_min_sequence: decision.memtable_min_sequence,
445        memtable_max_sequence: decision.memtable_max_sequence,
446        ..Default::default()
447    }
448}
449
450fn is_sink_scan(query_ctx: &QueryContext, region_id: RegionId) -> Result<bool> {
451    Ok(
452        FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())?
453            .is_some_and(|exts| exts.sink_table_id == Some(region_id.table_id())),
454    )
455}
456
457fn apply_cached_snapshot_to_request(
458    query_ctx: &QueryContext,
459    region_id: RegionId,
460    is_sink_scan: bool,
461    scan_request: &mut ScanRequest,
462) {
463    if is_sink_scan {
464        return;
465    }
466
467    if let Some(snapshot_sequence) = query_ctx.get_snapshot(region_id.as_u64()) {
468        // Reuse the previously bound per-region snapshot instead of rebinding a new
469        // upper bound on scan open. This refresh is still needed at scan time because
470        // the provider's cached request may have been built before another source scan
471        // bound the shared query-level snapshot into `query_ctx`.
472        scan_request.memtable_max_sequence = Some(snapshot_sequence);
473        scan_request.snapshot_on_scan = false;
474    }
475}
476
477fn bind_snapshot_bound_region_seq(
478    query_ctx: &QueryContext,
479    region_id: RegionId,
480    snapshot_sequence: u64,
481) -> Result<u64> {
482    if let Some(existing) = query_ctx.get_snapshot(region_id.as_u64()) {
483        if existing != snapshot_sequence {
484            return crate::error::ConflictingSnapshotSequenceSnafu {
485                region_id,
486                existing,
487                new: snapshot_sequence,
488            }
489            .fail();
490        }
491        Ok(existing)
492    } else {
493        query_ctx.set_snapshot(region_id.as_u64(), snapshot_sequence);
494        Ok(snapshot_sequence)
495    }
496}
497
498#[async_trait]
499impl TableProviderFactory for DummyTableProviderFactory {
500    async fn create(
501        &self,
502        region_id: RegionId,
503        engine: RegionEngineRef,
504        ctx: Option<QueryContextRef>,
505    ) -> Result<Arc<dyn TableProvider>> {
506        let provider = self.create_table_provider(region_id, engine, ctx).await?;
507        Ok(Arc::new(provider))
508    }
509}
510
511#[async_trait]
512pub trait TableProviderFactory: Send + Sync {
513    async fn create(
514        &self,
515        region_id: RegionId,
516        engine: RegionEngineRef,
517        ctx: Option<QueryContextRef>,
518    ) -> Result<Arc<dyn TableProvider>>;
519}
520
521pub type TableProviderFactoryRef = Arc<dyn TableProviderFactory>;
522
523/// A dummy catalog manager that always returns empty results.
524///
525/// Used to fill the arg of `QueryEngineFactory::new_with_plugins` in datanode.
526pub struct DummyCatalogManager;
527
528impl DummyCatalogManager {
529    /// Returns a new `CatalogManagerRef` instance.
530    pub fn arc() -> CatalogManagerRef {
531        Arc::new(Self)
532    }
533}
534
535#[async_trait::async_trait]
536impl CatalogManager for DummyCatalogManager {
537    fn as_any(&self) -> &dyn Any {
538        self
539    }
540
541    async fn catalog_names(&self) -> CatalogResult<Vec<String>> {
542        Ok(vec![])
543    }
544
545    async fn schema_names(
546        &self,
547        _catalog: &str,
548        _query_ctx: Option<&QueryContext>,
549    ) -> CatalogResult<Vec<String>> {
550        Ok(vec![])
551    }
552
553    async fn table_names(
554        &self,
555        _catalog: &str,
556        _schema: &str,
557        _query_ctx: Option<&QueryContext>,
558    ) -> CatalogResult<Vec<String>> {
559        Ok(vec![])
560    }
561
562    async fn catalog_exists(&self, _catalog: &str) -> CatalogResult<bool> {
563        Ok(false)
564    }
565
566    async fn schema_exists(
567        &self,
568        _catalog: &str,
569        _schema: &str,
570        _query_ctx: Option<&QueryContext>,
571    ) -> CatalogResult<bool> {
572        Ok(false)
573    }
574
575    async fn table_exists(
576        &self,
577        _catalog: &str,
578        _schema: &str,
579        _table: &str,
580        _query_ctx: Option<&QueryContext>,
581    ) -> CatalogResult<bool> {
582        Ok(false)
583    }
584
585    async fn table(
586        &self,
587        _catalog: &str,
588        _schema: &str,
589        _table_name: &str,
590        _query_ctx: Option<&QueryContext>,
591    ) -> CatalogResult<Option<TableRef>> {
592        Ok(None)
593    }
594
595    async fn table_id(
596        &self,
597        _catalog: &str,
598        _schema: &str,
599        _table_name: &str,
600        _query_ctx: Option<&QueryContext>,
601    ) -> CatalogResult<Option<TableId>> {
602        Ok(None)
603    }
604
605    async fn table_info_by_id(&self, _table_id: TableId) -> CatalogResult<Option<TableInfoRef>> {
606        Ok(None)
607    }
608
609    async fn tables_by_ids(
610        &self,
611        _catalog: &str,
612        _schema: &str,
613        _table_ids: &[TableId],
614    ) -> CatalogResult<Vec<TableRef>> {
615        Ok(vec![])
616    }
617
618    fn tables<'a>(
619        &'a self,
620        _catalog: &'a str,
621        _schema: &'a str,
622        _query_ctx: Option<&'a QueryContext>,
623    ) -> BoxStream<'a, CatalogResult<TableRef>> {
624        Box::pin(futures::stream::empty())
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use std::collections::HashMap;
631    use std::sync::{Arc, RwLock};
632
633    use common_error::ext::ErrorExt;
634    use common_error::status_code::StatusCode;
635    use session::context::QueryContextBuilder;
636
637    use super::*;
638    use crate::error::Error;
639    use crate::options::{
640        FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, FLOW_RETURN_REGION_SEQ,
641        FLOW_SINK_TABLE_ID,
642    };
643
644    fn test_region_id() -> RegionId {
645        RegionId::new(1024, 1)
646    }
647
648    #[test]
649    fn test_scan_request_from_query_context_uses_snapshot_bound_intent() {
650        let region_id = test_region_id();
651        let query_ctx = QueryContextBuilder::default()
652            .extensions(HashMap::from([(
653                "flow.return_region_seq".to_string(),
654                "true".to_string(),
655            )]))
656            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
657                region_id.as_u64(),
658                42_u64,
659            )]))))
660            .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
661                region_id.as_u64(),
662                7_u64,
663            )]))))
664            .build();
665
666        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
667
668        assert!(!request.snapshot_on_scan);
669        assert_eq!(request.memtable_max_sequence, Some(42));
670        assert_eq!(request.sst_min_sequence, Some(7));
671    }
672
673    #[test]
674    fn test_terminal_watermark_context_source_and_sink_scan_semantics() {
675        let region_id = test_region_id();
676        let query_ctx = QueryContextBuilder::default()
677            .extensions(HashMap::from([(
678                FLOW_RETURN_REGION_SEQ.to_string(),
679                "true".to_string(),
680            )]))
681            .build();
682
683        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
684
685        assert!(request.snapshot_on_scan);
686        assert_eq!(request.memtable_min_sequence, None);
687        assert_eq!(request.memtable_max_sequence, None);
688        assert_eq!(request.sst_min_sequence, None);
689
690        let query_ctx = QueryContextBuilder::default()
691            .extensions(HashMap::from([
692                (FLOW_RETURN_REGION_SEQ.to_string(), "true".to_string()),
693                (
694                    FLOW_SINK_TABLE_ID.to_string(),
695                    region_id.table_id().to_string(),
696                ),
697            ]))
698            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
699                region_id.as_u64(),
700                88_u64,
701            )]))))
702            .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
703                region_id.as_u64(),
704                77_u64,
705            )]))))
706            .build();
707
708        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
709
710        assert!(!request.snapshot_on_scan);
711        assert_eq!(request.memtable_min_sequence, None);
712        assert_eq!(request.memtable_max_sequence, None);
713        assert_eq!(request.sst_min_sequence, None);
714    }
715
716    #[test]
717    fn test_scan_request_from_incremental_context_uses_snapshot_bound_intent() {
718        let region_id = test_region_id();
719        let query_ctx = QueryContextBuilder::default()
720            .extensions(HashMap::from([(
721                "flow.incremental_after_seqs".to_string(),
722                format!(r#"{{"{}":10}}"#, region_id.as_u64()),
723            )]))
724            .build();
725
726        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
727
728        assert!(request.snapshot_on_scan);
729        assert_eq!(request.memtable_min_sequence, Some(10));
730        assert_eq!(request.memtable_max_sequence, None);
731    }
732
733    #[test]
734    fn test_scan_request_from_query_context_keeps_snapshot_fields() {
735        let region_id = test_region_id();
736        let query_ctx = QueryContextBuilder::default()
737            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
738                region_id.as_u64(),
739                100,
740            )]))))
741            .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
742                region_id.as_u64(),
743                90,
744            )]))))
745            .build();
746
747        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
748        assert_eq!(request.memtable_max_sequence, Some(100));
749        assert_eq!(request.sst_min_sequence, Some(90));
750        assert_eq!(request.memtable_min_sequence, None);
751        assert!(!request.snapshot_on_scan);
752        assert!(!request.skip_sst_files);
753    }
754
755    #[test]
756    fn test_scan_request_from_query_context_reuses_existing_snapshot_for_incremental_scan() {
757        let region_id = test_region_id();
758        let query_ctx = QueryContextBuilder::default()
759            .extensions(HashMap::from([
760                (
761                    FLOW_INCREMENTAL_MODE.to_string(),
762                    "memtable_only".to_string(),
763                ),
764                (
765                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
766                    format!(r#"{{"{}":10}}"#, region_id.as_u64()),
767                ),
768            ]))
769            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
770                region_id.as_u64(),
771                42_u64,
772            )]))))
773            .build();
774
775        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
776
777        assert_eq!(request.memtable_min_sequence, Some(10));
778        assert_eq!(request.memtable_max_sequence, Some(42));
779        assert!(!request.snapshot_on_scan);
780        assert!(request.skip_sst_files);
781    }
782
783    #[test]
784    fn test_apply_cached_snapshot_to_request_updates_cached_scan_request() {
785        let region_id = test_region_id();
786        let query_ctx = QueryContextBuilder::default()
787            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
788                region_id.as_u64(),
789                88_u64,
790            )]))))
791            .build();
792        let mut request = ScanRequest {
793            snapshot_on_scan: true,
794            ..Default::default()
795        };
796
797        apply_cached_snapshot_to_request(&query_ctx, region_id, false, &mut request);
798
799        assert_eq!(request.memtable_max_sequence, Some(88));
800        assert!(!request.snapshot_on_scan);
801    }
802
803    #[test]
804    fn test_apply_cached_snapshot_to_request_skips_sink_scan() {
805        let region_id = test_region_id();
806        let query_ctx = QueryContextBuilder::default()
807            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
808                region_id.as_u64(),
809                88_u64,
810            )]))))
811            .build();
812        let mut request = ScanRequest {
813            snapshot_on_scan: true,
814            ..Default::default()
815        };
816
817        apply_cached_snapshot_to_request(&query_ctx, region_id, true, &mut request);
818
819        assert_eq!(request.memtable_max_sequence, None);
820        assert!(request.snapshot_on_scan);
821    }
822
823    #[test]
824    fn test_bind_snapshot_bound_region_seq_reuses_existing_snapshot() {
825        let region_id = test_region_id();
826        let query_ctx = QueryContextBuilder::default()
827            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
828                region_id.as_u64(),
829                42_u64,
830            )]))))
831            .build();
832
833        let err = bind_snapshot_bound_region_seq(&query_ctx, region_id, 99).unwrap_err();
834
835        assert!(matches!(err, Error::ConflictingSnapshotSequence { .. }));
836        assert_eq!(query_ctx.get_snapshot(region_id.as_u64()), Some(42));
837    }
838
839    #[test]
840    fn test_bind_snapshot_bound_region_seq_sets_snapshot_once() {
841        let region_id = test_region_id();
842        let query_ctx = QueryContextBuilder::default().build();
843
844        let seq = bind_snapshot_bound_region_seq(&query_ctx, region_id, 99).unwrap();
845
846        assert_eq!(seq, 99);
847        assert_eq!(query_ctx.get_snapshot(region_id.as_u64()), Some(99));
848    }
849
850    #[test]
851    fn test_scan_request_from_query_context_applies_incremental_after_seq_for_source_scan() {
852        let region_id = test_region_id();
853        let query_ctx = QueryContextBuilder::default()
854            .extensions(HashMap::from([
855                (
856                    FLOW_INCREMENTAL_MODE.to_string(),
857                    "memtable_only".to_string(),
858                ),
859                (
860                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
861                    format!(r#"{{"{}":55}}"#, region_id.as_u64()),
862                ),
863            ]))
864            .build();
865
866        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
867        assert_eq!(request.memtable_min_sequence, Some(55));
868        assert_eq!(request.sst_min_sequence, None);
869        assert!(request.skip_sst_files);
870    }
871
872    #[test]
873    fn test_scan_request_from_query_context_does_not_apply_incremental_for_sink_table() {
874        let region_id = test_region_id();
875        let query_ctx = QueryContextBuilder::default()
876            .extensions(HashMap::from([
877                (
878                    FLOW_INCREMENTAL_MODE.to_string(),
879                    "memtable_only".to_string(),
880                ),
881                (
882                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
883                    format!(r#"{{"{}":55}}"#, region_id.as_u64()),
884                ),
885                (
886                    FLOW_SINK_TABLE_ID.to_string(),
887                    region_id.table_id().to_string(),
888                ),
889            ]))
890            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
891                region_id.as_u64(),
892                88_u64,
893            )]))))
894            .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
895                region_id.as_u64(),
896                77_u64,
897            )]))))
898            .build();
899
900        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
901        assert_eq!(request.memtable_min_sequence, None);
902        assert_eq!(request.memtable_max_sequence, None);
903        assert_eq!(request.sst_min_sequence, None);
904        assert!(!request.skip_sst_files);
905        assert!(!request.snapshot_on_scan);
906    }
907
908    #[test]
909    fn test_scan_request_from_query_context_rejects_missing_memtable_only_region() {
910        let region_id = test_region_id();
911        let query_ctx = QueryContextBuilder::default()
912            .extensions(HashMap::from([
913                (
914                    FLOW_INCREMENTAL_MODE.to_string(),
915                    "memtable_only".to_string(),
916                ),
917                (
918                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
919                    r#"{"9":55}"#.to_string(),
920                ),
921            ]))
922            .build();
923
924        let err = scan_request_from_query_context(region_id, &query_ctx).unwrap_err();
925        assert!(matches!(err, Error::InvalidQueryContextExtension { .. }));
926    }
927
928    #[test]
929    fn test_scan_request_from_query_context_rejects_invalid_incremental_json() {
930        let region_id = test_region_id();
931        let query_ctx = QueryContextBuilder::default()
932            .extensions(HashMap::from([(
933                FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
934                "not-json".to_string(),
935            )]))
936            .build();
937
938        let err = scan_request_from_query_context(region_id, &query_ctx).unwrap_err();
939        assert!(matches!(err, Error::InvalidQueryContextExtension { .. }));
940        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
941    }
942
943    #[test]
944    fn test_scan_request_from_query_context_rejects_invalid_sink_table_id() {
945        let region_id = test_region_id();
946        let query_ctx = QueryContextBuilder::default()
947            .extensions(HashMap::from([(
948                FLOW_SINK_TABLE_ID.to_string(),
949                "abc".to_string(),
950            )]))
951            .build();
952
953        let err = scan_request_from_query_context(region_id, &query_ctx).unwrap_err();
954        assert!(matches!(err, Error::InvalidQueryContextExtension { .. }));
955        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
956    }
957}